Swap user credentials for an OAuth token from GitHub
parent
1bd5d88b02
commit
3b01d26d67
|
@ -51,10 +51,12 @@ class GitDownloader extends VcsDownloader
|
|||
$this->io->write(" Checking out ".$ref);
|
||||
$command = 'cd %s && git remote set-url composer %s && git fetch composer && git fetch --tags composer';
|
||||
|
||||
// capture username/password from github URL if there is one
|
||||
$this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output);
|
||||
if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) {
|
||||
$this->io->setAuthorization('github.com', $match[1], $match[2]);
|
||||
if (!$this->io->hasAuthorization('github.com')) {
|
||||
// capture username/password from github URL if there is one
|
||||
$this->process->execute(sprintf('cd %s && git remote -v', escapeshellarg($path)), $output);
|
||||
if (preg_match('{^composer\s+https://(.+):(.+)@github.com/}im', $output, $match)) {
|
||||
$this->io->setAuthorization('github.com', $match[1], $match[2]);
|
||||
}
|
||||
}
|
||||
|
||||
$commandCallable = function($url) use ($ref, $path, $command) {
|
||||
|
@ -327,7 +329,7 @@ class GitDownloader extends VcsDownloader
|
|||
|
||||
protected function sanitizeUrl($message)
|
||||
{
|
||||
return preg_match('{://(.+?):.+?@}', '://$1:***@', $message);
|
||||
return preg_replace('{://(.+?):.+?@}', '://$1:***@', $message);
|
||||
}
|
||||
|
||||
protected function setPushUrl(PackageInterface $package, $path)
|
||||
|
|
|
@ -17,4 +17,15 @@ namespace Composer\Downloader;
|
|||
*/
|
||||
class TransportException extends \Exception
|
||||
{
|
||||
protected $headers;
|
||||
|
||||
public function setHeaders($headers)
|
||||
{
|
||||
$this->headers = $headers;
|
||||
}
|
||||
|
||||
public function getHeaders()
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -235,6 +235,62 @@ class GitHubDriver extends VcsDriver
|
|||
return 'git@github.com:'.$this->owner.'/'.$this->repository.'.git';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function getContents($url, $tryClone = false)
|
||||
{
|
||||
try {
|
||||
return parent::getContents($url);
|
||||
} catch (TransportException $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
case 404:
|
||||
if (!$this->io->isInteractive() && $tryClone) {
|
||||
return $this->attemptCloneFallback($e);
|
||||
}
|
||||
|
||||
$this->io->write('Your GitHub credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>):');
|
||||
$this->authorizeOauth();
|
||||
|
||||
return parent::getContents($url);
|
||||
|
||||
case 403:
|
||||
if (!$this->io->isInteractive() && $tryClone) {
|
||||
return $this->attemptCloneFallback($e);
|
||||
}
|
||||
|
||||
$rateLimited = false;
|
||||
foreach ($e->getHeaders() as $header) {
|
||||
if (preg_match('{^X-RateLimit-Remaining: *0$}i', trim($header))) {
|
||||
$rateLimited = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->io->hasAuthorization($this->originUrl)) {
|
||||
if (!$this->io->isInteractive()) {
|
||||
$this->io->write('<error>GitHub API limit exhausted. Failed to get metadata for the '.$this->url.' repository, try running in interactive mode so that you can enter your GitHub credentials to increase the API limit</error>');
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->io->write('API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>):');
|
||||
$this->authorizeOauth();
|
||||
|
||||
return parent::getContents($url);
|
||||
}
|
||||
|
||||
if ($rateLimited) {
|
||||
$this->io->write('<error>GitHub API limit exhausted. You are already authorized so you will have to wait a while before doing more requests</error>');
|
||||
}
|
||||
|
||||
throw $e;
|
||||
|
||||
default:
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch root identifier from GitHub
|
||||
*
|
||||
|
@ -243,73 +299,83 @@ class GitHubDriver extends VcsDriver
|
|||
protected function fetchRootIdentifier()
|
||||
{
|
||||
$repoDataUrl = 'https://api.github.com/repos/'.$this->owner.'/'.$this->repository;
|
||||
$attemptCounter = 0;
|
||||
while (null === $this->rootIdentifier) {
|
||||
if (5 == $attemptCounter++) {
|
||||
throw new \RuntimeException("Either you have entered invalid credentials or this GitHub repository does not exists (404)");
|
||||
}
|
||||
try {
|
||||
$repoData = JsonFile::parseJson($this->getContents($repoDataUrl), $repoDataUrl);
|
||||
if (isset($repoData['default_branch'])) {
|
||||
$this->rootIdentifier = $repoData['default_branch'];
|
||||
} elseif (isset($repoData['master_branch'])) {
|
||||
$this->rootIdentifier = $repoData['master_branch'];
|
||||
} else {
|
||||
$this->rootIdentifier = 'master';
|
||||
}
|
||||
$this->hasIssues = !empty($repoData['has_issues']);
|
||||
} catch (TransportException $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
case 404:
|
||||
$this->isPrivate = true;
|
||||
|
||||
try {
|
||||
// If this repository may be private (hard to say for sure,
|
||||
// GitHub returns 404 for private repositories) and we
|
||||
// cannot ask for authentication credentials (because we
|
||||
// are not interactive) then we fallback to GitDriver.
|
||||
$this->gitDriver = new GitDriver(
|
||||
$this->generateSshUrl(),
|
||||
$this->io,
|
||||
$this->config,
|
||||
$this->process,
|
||||
$this->remoteFilesystem
|
||||
);
|
||||
$this->gitDriver->initialize();
|
||||
$repoData = JsonFile::parseJson($this->getContents($repoDataUrl, true), $repoDataUrl);
|
||||
if (null === $repoData && null !== $this->gitDriver) {
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->gitDriver = null;
|
||||
if (!$this->io->isInteractive()) {
|
||||
$this->io->write('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your username and password</error>');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->io->write('Authentication required (<info>'.$this->url.'</info>):');
|
||||
$username = $this->io->ask('Username: ');
|
||||
$password = $this->io->askAndHideAnswer('Password: ');
|
||||
$this->io->setAuthorization($this->originUrl, $username, $password);
|
||||
break;
|
||||
$this->isPrivate = !empty($repoData['private']);
|
||||
if (isset($repoData['default_branch'])) {
|
||||
$this->rootIdentifier = $repoData['default_branch'];
|
||||
} elseif (isset($repoData['master_branch'])) {
|
||||
$this->rootIdentifier = $repoData['master_branch'];
|
||||
} else {
|
||||
$this->rootIdentifier = 'master';
|
||||
}
|
||||
$this->hasIssues = !empty($repoData['has_issues']);
|
||||
}
|
||||
|
||||
case 403:
|
||||
if (!$this->io->hasAuthorization($this->originUrl)) {
|
||||
if (!$this->io->isInteractive()) {
|
||||
$this->io->write('<error>API limit exhausted. Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your username and password to increase the API limit</error>');
|
||||
throw $e;
|
||||
}
|
||||
$this->io->write('API limit exhausted. Authentication required for larger API limit (<info>'.$this->url.'</info>):');
|
||||
$username = $this->io->ask('Username: ');
|
||||
$password = $this->io->askAndHideAnswer('Password: ');
|
||||
$this->io->setAuthorization($this->originUrl, $username, $password);
|
||||
}
|
||||
break;
|
||||
protected function attemptCloneFallback()
|
||||
{
|
||||
$this->isPrivate = true;
|
||||
|
||||
default:
|
||||
throw $e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
// If this repository may be private (hard to say for sure,
|
||||
// GitHub returns 404 for private repositories) and we
|
||||
// cannot ask for authentication credentials (because we
|
||||
// are not interactive) then we fallback to GitDriver.
|
||||
$this->gitDriver = new GitDriver(
|
||||
$this->generateSshUrl(),
|
||||
$this->io,
|
||||
$this->config,
|
||||
$this->process,
|
||||
$this->remoteFilesystem
|
||||
);
|
||||
$this->gitDriver->initialize();
|
||||
|
||||
return;
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->gitDriver = null;
|
||||
|
||||
$this->io->write('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your GitHub credentials</error>');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorizeOAuth()
|
||||
{
|
||||
$attemptCounter = 0;
|
||||
|
||||
$this->io->write('The credentials will be swapped for an OAuth token stored in '.$this->config->get('home').'/config.json, your password will not be stored');
|
||||
$this->io->write('To revoke access to this token you can visit https://github.com/settings/applications');
|
||||
while ($attemptCounter++ < 5) {
|
||||
try {
|
||||
$username = $this->io->ask('Username: ');
|
||||
$password = $this->io->askAndHideAnswer('Password: ');
|
||||
$this->io->setAuthorization($this->originUrl, $username, $password);
|
||||
|
||||
$contents = JsonFile::parseJson($this->remoteFilesystem->getContents($this->originUrl, 'https://api.github.com/authorizations', false, array(
|
||||
'http' => array(
|
||||
'method' => 'POST',
|
||||
'header' => "Content-Type: application/json\r\n",
|
||||
'content' => '{"scopes":["repo"],"note":"Composer","note_url":"https://getcomposer.org/"}',
|
||||
)
|
||||
)));
|
||||
} catch (TransportException $e) {
|
||||
if (401 === $e->getCode()) {
|
||||
$this->io->write('Invalid credentials.');
|
||||
continue;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->io->setAuthorization($this->originUrl, $contents['token'], 'x-oauth-basic');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,12 +50,13 @@ class RemoteFilesystem
|
|||
* @param string $fileUrl The file URL
|
||||
* @param string $fileName the local filename
|
||||
* @param boolean $progress Display the progression
|
||||
* @param array $options Additional context options
|
||||
*
|
||||
* @return bool true
|
||||
*/
|
||||
public function copy($originUrl, $fileUrl, $fileName, $progress = true)
|
||||
public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = array())
|
||||
{
|
||||
$this->get($originUrl, $fileUrl, $fileName, $progress);
|
||||
$this->get($originUrl, $fileUrl, $options, $fileName, $progress);
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
|
@ -66,12 +67,13 @@ class RemoteFilesystem
|
|||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param boolean $progress Display the progression
|
||||
* @param array $options Additional context options
|
||||
*
|
||||
* @return string The content
|
||||
*/
|
||||
public function getContents($originUrl, $fileUrl, $progress = true)
|
||||
public function getContents($originUrl, $fileUrl, $progress = true, $options = array())
|
||||
{
|
||||
$this->get($originUrl, $fileUrl, null, $progress);
|
||||
$this->get($originUrl, $fileUrl, $options, null, $progress);
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
|
@ -79,14 +81,15 @@ class RemoteFilesystem
|
|||
/**
|
||||
* Get file content or copy action.
|
||||
*
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param string $fileName the local filename
|
||||
* @param boolean $progress Display the progression
|
||||
* @param string $originUrl The origin URL
|
||||
* @param string $fileUrl The file URL
|
||||
* @param array $additionalOptions context options
|
||||
* @param string $fileName the local filename
|
||||
* @param boolean $progress Display the progression
|
||||
*
|
||||
* @throws TransportException When the file could not be downloaded
|
||||
*/
|
||||
protected function get($originUrl, $fileUrl, $fileName = null, $progress = true)
|
||||
protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true)
|
||||
{
|
||||
$this->bytesMax = 0;
|
||||
$this->result = null;
|
||||
|
@ -96,7 +99,7 @@ class RemoteFilesystem
|
|||
$this->progress = $progress;
|
||||
$this->lastProgress = null;
|
||||
|
||||
$options = $this->getOptionsForUrl($originUrl);
|
||||
$options = $this->getOptionsForUrl($originUrl, $additionalOptions);
|
||||
$ctx = StreamContextFactory::getContext($options, array('notification' => array($this, 'callbackGet')));
|
||||
|
||||
if ($this->progress) {
|
||||
|
@ -110,11 +113,20 @@ class RemoteFilesystem
|
|||
}
|
||||
$errorMessage .= preg_replace('{^file_get_contents\(.*?\): }', '', $msg);
|
||||
});
|
||||
$result = file_get_contents($fileUrl, false, $ctx);
|
||||
try {
|
||||
$result = file_get_contents($fileUrl, false, $ctx);
|
||||
} catch (\Exception $e) {
|
||||
if ($e instanceof TransportException && !empty($http_response_header[0])) {
|
||||
$e->setHeaders($http_response_header);
|
||||
}
|
||||
}
|
||||
if ($errorMessage && !ini_get('allow_url_fopen')) {
|
||||
$errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')';
|
||||
}
|
||||
restore_error_handler();
|
||||
if (isset($e)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// fix for 5.4.0 https://bugs.php.net/bug.php?id=61336
|
||||
if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ 404}i', $http_response_header[0])) {
|
||||
|
@ -239,9 +251,9 @@ class RemoteFilesystem
|
|||
}
|
||||
}
|
||||
|
||||
protected function getOptionsForUrl($originUrl)
|
||||
protected function getOptionsForUrl($originUrl, $additionalOptions)
|
||||
{
|
||||
$options['http']['header'] = sprintf(
|
||||
$header = sprintf(
|
||||
"User-Agent: Composer/%s (%s; %s; PHP %s.%s.%s)\r\n",
|
||||
Composer::VERSION,
|
||||
php_uname('s'),
|
||||
|
@ -251,16 +263,22 @@ class RemoteFilesystem
|
|||
PHP_RELEASE_VERSION
|
||||
);
|
||||
if (extension_loaded('zlib')) {
|
||||
$options['http']['header'] .= 'Accept-Encoding: gzip'."\r\n";
|
||||
$header .= 'Accept-Encoding: gzip'."\r\n";
|
||||
}
|
||||
|
||||
if ($this->io->hasAuthorization($originUrl)) {
|
||||
$auth = $this->io->getAuthorization($originUrl);
|
||||
$authStr = base64_encode($auth['username'] . ':' . $auth['password']);
|
||||
$options['http']['header'] .= "Authorization: Basic $authStr\r\n";
|
||||
$header .= "Authorization: Basic $authStr\r\n";
|
||||
}
|
||||
|
||||
$options = array_replace_recursive($options, $this->options);
|
||||
$options = array_replace_recursive($this->options, $additionalOptions);
|
||||
|
||||
if (isset($options['http']['header'])) {
|
||||
$options['http']['header'] = rtrim($options['http']['header'], "\r\n") . "\r\n" . $header;
|
||||
} else {
|
||||
$options['http']['header'] = $header;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue