1
0
Fork 0

Refactor OAuth acquisition code to generalize it

pull/1238/merge
Jordi Boggiano 2012-10-21 17:56:57 +02:00
parent bf5f34a114
commit 39e69a3b12
10 changed files with 170 additions and 102 deletions

View File

@ -12,10 +12,12 @@
namespace Composer\Downloader;
use Composer\Config;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Util\Filesystem;
use Composer\Util\GitHub;
use Composer\Util\RemoteFilesystem;
/**
@ -28,6 +30,7 @@ use Composer\Util\RemoteFilesystem;
class FileDownloader implements DownloaderInterface
{
protected $io;
protected $config;
protected $rfs;
protected $filesystem;
@ -36,9 +39,10 @@ class FileDownloader implements DownloaderInterface
*
* @param IOInterface $io The IO instance
*/
public function __construct(IOInterface $io, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
public function __construct(IOInterface $io, Config $config, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
{
$this->io = $io;
$this->config = $config;
$this->rfs = $rfs ?: new RemoteFilesystem($io);
$this->filesystem = $filesystem ?: new Filesystem();
}
@ -70,7 +74,18 @@ class FileDownloader implements DownloaderInterface
$processUrl = $this->processUrl($package, $url);
try {
$this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName);
try {
$this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName);
} catch (TransportException $e) {
if (404 === $e->getCode() && 'github.com' === parse_url($processUrl, PHP_URL_HOST)) {
$message = "\n".'Could not fetch '.$processUrl.', enter your GitHub credentials to access private repos';
$gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs);
$gitHubUtil->authorizeOAuth('github.com', $message);
$this->rfs->copy(parse_url($processUrl, PHP_URL_HOST), $processUrl, $fileName);
} else {
throw $e;
}
}
if (!file_exists($fileName)) {
throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'

View File

@ -13,6 +13,7 @@
namespace Composer\Downloader;
use Composer\Package\PackageInterface;
use Composer\Util\GitHub;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -283,32 +284,21 @@ class GitDownloader extends VcsDownloader
$command = call_user_func($commandCallable, $url);
if (0 !== $this->process->execute($command, $handler)) {
if (preg_match('{^git@github.com:(.+?)\.git$}i', $url, $match) && $this->io->isInteractive()) {
// private github repository without git access, try https with auth
$retries = 3;
$retrying = false;
do {
if ($retrying) {
$this->io->write('Invalid credentials');
}
if (!$this->io->hasAuthorization('github.com') || $retrying) {
$username = $this->io->ask('Username: ');
$password = $this->io->askAndHideAnswer('Password: ');
$this->io->setAuthorization('github.com', $username, $password);
}
// private github repository without git access, try https with auth
if (preg_match('{^git@(github.com):(.+?)\.git$}i', $url, $match) && $this->io->isInteractive()) {
if (!$this->io->hasAuthorization($match[1])) {
$message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos';
$gitHubUtil = new GitHub($this->io, $this->config, $this->process);
$gitHubUtil->authorizeOAuth($match[1], $message);
}
$auth = $this->io->getAuthorization('github.com');
$url = 'https://'.$auth['username'] . ':' . $auth['password'] . '@github.com/'.$match[1].'.git';
$auth = $this->io->getAuthorization($match[1]);
$url = 'https://'.$auth['username'] . ':' . $auth['password'] . '@'.$match[1].'/'.$match[2].'.git';
$command = call_user_func($commandCallable, $url);
if (0 === $this->process->execute($command, $handler)) {
return;
}
if (null !== $path) {
$this->filesystem->removeDirectory($path);
}
$retrying = true;
} while (--$retries);
$command = call_user_func($commandCallable, $url);
if (0 === $this->process->execute($command, $handler)) {
return;
}
}
if (null !== $path) {

View File

@ -12,6 +12,7 @@
namespace Composer\Downloader;
use Composer\Config;
use Composer\Util\ProcessExecutor;
use Composer\IO\IOInterface;
use ZipArchive;
@ -23,10 +24,10 @@ class ZipDownloader extends ArchiveDownloader
{
protected $process;
public function __construct(IOInterface $io, ProcessExecutor $process = null)
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null)
{
$this->process = $process ?: new ProcessExecutor;
parent::__construct($io);
parent::__construct($io, $config);
}
protected function extract($file, $path)

View File

@ -234,10 +234,10 @@ class Factory
$dm->setDownloader('git', new Downloader\GitDownloader($io, $config));
$dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config));
$dm->setDownloader('hg', new Downloader\HgDownloader($io, $config));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io));
$dm->setDownloader('tar', new Downloader\TarDownloader($io));
$dm->setDownloader('phar', new Downloader\PharDownloader($io));
$dm->setDownloader('file', new Downloader\FileDownloader($io));
$dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config));
$dm->setDownloader('tar', new Downloader\TarDownloader($io, $config));
$dm->setDownloader('phar', new Downloader\PharDownloader($io, $config));
$dm->setDownloader('file', new Downloader\FileDownloader($io, $config));
return $dm;
}

View File

@ -17,6 +17,7 @@ use Composer\Json\JsonFile;
use Composer\Cache;
use Composer\IO\IOInterface;
use Composer\Util\RemoteFilesystem;
use Composer\Util\GitHub;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -255,8 +256,7 @@ class GitHubDriver extends VcsDriver
return $this->attemptCloneFallback($e);
}
$this->io->write('Your GitHub credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>):');
$this->authorizeOauth();
$this->authorizeOAuth('Your GitHub credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
return parent::getContents($url);
@ -278,8 +278,7 @@ class GitHubDriver extends VcsDriver
throw $e;
}
$this->io->write('API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>):');
$this->authorizeOauth();
$this->authorizeOAuth('API limit exhausted. Enter your GitHub credentials to get a larger API limit (<info>'.$this->url.'</info>)');
return parent::getContents($url);
}
@ -348,61 +347,9 @@ class GitHubDriver extends VcsDriver
}
}
protected function authorizeOAuth()
protected function authorizeOAuth($message)
{
// If available use token from git config
if (0 === $this->process->execute('git config github.accesstoken', $output)) {
$this->io->write('Using Github OAuth token stored in git config (github.accesstoken)');
$this->io->setAuthorization($this->originUrl, $output[0], 'x-oauth-basic');
return;
}
$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);
// build up OAuth app name
$appName = 'Composer';
if (0 === $this->process->execute('hostname', $output)) {
$appName .= ' on ' . trim($output);
}
$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' => json_encode(array(
'scopes' => array('repo'),
'note' => $appName,
'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');
// store value in user config
$githubTokens = $this->config->get('github-oauth') ?: array();
$githubTokens[$this->originUrl] = $contents['token'];
$this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens);
return;
}
throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting.");
$gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->remoteFilesystem);
$gitHubUtil->authorizeOAuth($this->originUrl, $message);
}
}

View File

@ -36,13 +36,13 @@ abstract class VcsDriver implements VcsDriverInterface
/**
* Constructor.
*
* @param array $repoConfig The repository configuration
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param callable $remoteFilesystem Remote Filesystem, injectable for mocking
* @param array $repoConfig The repository configuration
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
*/
final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, $remoteFilesystem = null)
final public function __construct(array $repoConfig, IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
{
$this->url = $repoConfig['url'];
$this->originUrl = $repoConfig['url'];

View File

@ -0,0 +1,113 @@
<?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;
use Composer\IO\IOInterface;
use Composer\Config;
use Composer\Downloader\TransportException;
use Composer\Json\JsonFile;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class GitHub
{
protected $io;
protected $config;
protected $process;
protected $remoteFilesystem;
/**
* Constructor.
*
* @param IOInterface $io The IO instance
* @param Config $config The composer configuration
* @param ProcessExecutor $process Process instance, injectable for mocking
* @param RemoteFilesystem $remoteFilesystem Remote Filesystem, injectable for mocking
*/
public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, RemoteFilesystem $remoteFilesystem = null)
{
$this->io = $io;
$this->config = $config;
$this->process = $process ?: new ProcessExecutor;
$this->remoteFilesystem = $remoteFilesystem ?: new RemoteFilesystem($io);
}
/**
* Authorizes a GitHub domain via OAuth
*
* @param string $originUrl The host this GitHub instance is located at
* @param string $message The reason this authorization is required
*/
public function authorizeOAuth($originUrl, $message = null)
{
// if available use token from git config
if (0 === $this->process->execute('git config github.accesstoken', $output)) {
$this->io->write('Using Github OAuth token stored in git config (github.accesstoken)');
$this->io->setAuthorization($originUrl, trim($output), 'x-oauth-basic');
return;
}
$attemptCounter = 0;
if ($message) {
$this->io->write($message);
}
$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($originUrl, $username, $password);
// build up OAuth app name
$appName = 'Composer';
if (0 === $this->process->execute('hostname', $output)) {
$appName .= ' on ' . trim($output);
}
$contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://api.github.com/authorizations', false, array(
'http' => array(
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(array(
'scopes' => array('repo'),
'note' => $appName,
'note_url' => 'https://getcomposer.org/',
)),
)
)));
} catch (TransportException $e) {
if (in_array($e->getCode(), array(403, 401))) {
$this->io->write('Invalid credentials.');
continue;
}
throw $e;
}
$this->io->setAuthorization($originUrl, $contents['token'], 'x-oauth-basic');
// store value in user config
$githubTokens = $this->config->get('github-oauth') ?: array();
$githubTokens[$originUrl] = $contents['token'];
$this->config->getConfigSource()->addConfigSetting('github-oauth', $githubTokens);
return;
}
throw new \RuntimeException("Invalid GitHub credentials 5 times in a row, aborting.");
}
}

View File

@ -22,7 +22,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase
->will($this->returnValue('http://example.com/script.js'))
;
$downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface')));
$downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config')));
$method = new \ReflectionMethod($downloader, 'getFileName');
$method->setAccessible(true);
@ -33,7 +33,7 @@ class ArchiveDownloaderTest extends \PHPUnit_Framework_TestCase
public function testProcessUrl()
{
$downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface')));
$downloader = $this->getMockForAbstractClass('Composer\Downloader\ArchiveDownloader', array($this->getMock('Composer\IO\IOInterface'), $this->getMock('Composer\Config')));
$method = new \ReflectionMethod($downloader, 'processUrl');
$method->setAccessible(true);

View File

@ -17,12 +17,13 @@ use Composer\Util\Filesystem;
class FileDownloaderTest extends \PHPUnit_Framework_TestCase
{
protected function getDownloader($io = null, $rfs = null)
protected function getDownloader($io = null, $config = null, $rfs = null)
{
$io = $io ?: $this->getMock('Composer\IO\IOInterface');
$config = $config ?: $this->getMock('Composer\Config');
$rfs = $rfs ?: $this->getMockBuilder('Composer\Util\RemoteFilesystem')->disableOriginalConstructor()->getMock();
return new FileDownloader($io, $rfs);
return new FileDownloader($io, $config, $rfs);
}
/**

View File

@ -32,7 +32,8 @@ class ZipDownloaderTest extends \PHPUnit_Framework_TestCase
;
$io = $this->getMock('Composer\IO\IOInterface');
$downloader = new ZipDownloader($io);
$config = $this->getMock('Composer\Config');
$downloader = new ZipDownloader($io, $config);
try {
$downloader->download($packageMock, sys_get_temp_dir().'/composer-zip-test');