Skip to content

Commit

Permalink
Merge branch 'master' of github.com:composer/composer into tls-config
Browse files Browse the repository at this point in the history
Conflicts:
	src/Composer/Util/RemoteFilesystem.php
  • Loading branch information
padraic committed Feb 27, 2014
2 parents a23d875 + 0d4c2bb commit 81b86ac
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 36 deletions.
4 changes: 2 additions & 2 deletions doc/04-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -684,8 +684,8 @@ The following options are supported:
`{"github.com": "oauthtoken"}` as the value of this option will use `oauthtoken`
to access private repositories on github and to circumvent the low IP-based
rate limiting of their API.
[Read more](articles/troubleshooting.md#api-rate-limit-and-two-factor-authentication)
on how to get an oauth token for GitHub.
[Read more](articles/troubleshooting.md#api-rate-limit-and-oauth-tokens)
on how to get an OAuth token for GitHub.
* **vendor-dir:** Defaults to `vendor`. You can install dependencies into a
different directory if you want to.
* **bin-dir:** Defaults to `vendor/bin`. If a project includes binaries, they
Expand Down
9 changes: 5 additions & 4 deletions doc/articles/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ Or, you can increase the limit with a command-line argument:
or ```HKEY_CURRENT_USER\Software\Microsoft\Command Processor```.
3. Check if it contains any path to non-existent file, if it's the case, just remove them.

## API rate limit and two factor authentication
## API rate limit and OAuth tokens

Because of GitHub's rate limits on their API it can happen that Composer prompts
for authentication asking your username and password so it can go ahead with its work.
Unfortunately this will not work if you enabled two factor authentication on
your GitHub account and to solve this issue you need to:

1. [Create](https://github.com/settings/applications) an oauth token on GitHub.
If you would prefer not to provide your GitHub credentials to Composer you can
manually create a token using the following procedure:

1. [Create](https://github.com/settings/applications) an OAuth token on GitHub.
[Read more](https://github.com/blog/1509-personal-api-tokens) on this.

2. Add it to the configuration running `composer config -g github-oauth.github.com <oauthtoken>`
Expand Down
11 changes: 11 additions & 0 deletions src/Composer/Downloader/TransportException.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
class TransportException extends \RuntimeException
{
protected $headers;
protected $response;

public function setHeaders($headers)
{
Expand All @@ -28,4 +29,14 @@ public function getHeaders()
{
return $this->headers;
}

public function setResponse($response)
{
$this->response = $response;
}

public function getResponse()
{
return $this->response;
}
}
5 changes: 5 additions & 0 deletions src/Composer/Installer.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@ public function run()
$eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD;
$this->eventDispatcher->dispatchCommandEvent($eventName, $this->devMode);
}

$vendorDir = $this->config->get('vendor-dir');
if (is_dir($vendorDir)) {
touch($vendorDir);
}
}

return 0;
Expand Down
89 changes: 77 additions & 12 deletions src/Composer/Util/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,30 +87,95 @@ public function authorizeOAuthInteractively($originUrl, $message = null)
$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->setAuthentication($originUrl, $username, $password);
if (empty($otp) || !$this->io->hasAuthentication($originUrl)) {
$username = $this->io->ask('Username: ');
$password = $this->io->askAndHideAnswer('Password: ');
$otp = null;

$this->io->setAuthentication($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://'. $apiUrl . '/authorizations', false, array(
$headers = array();
if ($otp) {
$headers = array('X-GitHub-OTP: ' . $otp);
}

// try retrieving an existing token with the same name
$contents = null;
$auths = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array(
'retry-auth-failure' => false,
'http' => array(
'method' => 'POST',
'follow_location' => false,
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(array(
'scopes' => array('repo'),
'note' => $appName,
'note_url' => 'https://getcomposer.org/',
)),
'header' => $headers
)
)));
foreach ($auths as $auth) {
if (
isset($auth['app']['name'])
&& 0 === strpos($auth['app']['name'], $appName)
&& $auth['app']['url'] === 'https://getcomposer.org/'
) {
$this->io->write('An existing OAuth token for Composer is present and will be reused');

$contents['token'] = $auth['token'];
break;
}
}

// no existing token, create one
if (empty($contents['token'])) {
$headers[] = array('Content-Type: application/json');

$contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array(
'retry-auth-failure' => false,
'http' => array(
'method' => 'POST',
'follow_location' => false,
'header' => $headers,
'content' => json_encode(array(
'scopes' => array('repo'),
'note' => $appName,
'note_url' => 'https://getcomposer.org/',
)),
)
)));
$this->io->write('Token successfully created');
}
} catch (TransportException $e) {
if (in_array($e->getCode(), array(403, 401))) {
// 401 when authentication was supplied, handle 2FA if required.
if ($this->io->hasAuthentication($originUrl)) {
$headerNames = array_map(function($header) {
return strtolower(strstr($header, ':', true));
}, $e->getHeaders());

if ($key = array_search('x-github-otp', $headerNames)) {
$headers = $e->getHeaders();
list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1)));

if ('required' === $required) {
$this->io->write('Two-factor Authentication');

if ('app' === $method) {
$this->io->write('Open the two-factor authentication app on your device to view your authentication code and verify your identity.');
}

if ('sms' === $method) {
$this->io->write('You have been sent an SMS message with an authentication code to verify your identity.');
}

$otp = $this->io->ask('Authentication Code: ');

continue;
}
}
}

$this->io->write('Invalid credentials.');
continue;
}
Expand Down
37 changes: 29 additions & 8 deletions src/Composer/Util/RemoteFilesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class RemoteFilesystem
private $options;
private $disableTls = false;
private $retryTls = true;
private $retryAuthFailure;

/**
* Constructor.
Expand Down Expand Up @@ -128,12 +129,19 @@ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $file
$this->fileName = $fileName;
$this->progress = $progress;
$this->lastProgress = null;
$this->retryAuthFailure = true;

// 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]));
}

if (isset($additionalOptions['retry-auth-failure'])) {
$this->retryAuthFailure = (bool) $additionalOptions['retry-auth-failure'];

unset($additionalOptions['retry-auth-failure']);
}

$options = $this->getOptionsForUrl($originUrl, $additionalOptions, $expectedCommonName);

if ($this->io->isDebug()) {
Expand All @@ -143,6 +151,9 @@ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $file
$fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['github-token'];
unset($options['github-token']);
}
if (isset($options['http'])) {
$options['http']['ignore_errors'] = true;
}
$ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet')));

if ($this->progress) {
Expand All @@ -164,6 +175,10 @@ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $file
if ($e instanceof TransportException && !empty($http_response_header[0])) {
$e->setHeaders($http_response_header);
}
if ($e instanceof TransportException && $result !== false) {
$e->setResponse($result);
}
$result = false;
}
if ($errorMessage && !ini_get('allow_url_fopen')) {
$errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')';
Expand All @@ -173,10 +188,16 @@ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $file
throw $e;
}

// fix for 5.4.0 https://bugs.php.net/bug.php?id=61336
// fail 4xx and 5xx responses and capture the response
if (!empty($http_response_header[0]) && preg_match('{^HTTP/\S+ ([45]\d\d)}i', $http_response_header[0], $match)) {
$result = false;
$errorCode = $match[1];
if (!$this->retry) {
$e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.$http_response_header[0].')', $errorCode);
$e->setHeaders($http_response_header);
$e->setResponse($result);
throw $e;
}
$result = false;
}

// decode gzip
Expand Down Expand Up @@ -272,6 +293,11 @@ protected function callbackGet($notificationCode, $severity, $message, $messageC
case STREAM_NOTIFY_FAILURE:
case STREAM_NOTIFY_AUTH_REQUIRED:
if (401 === $messageCode) {
// Bail if the caller is going to handle authentication failures itself.
if (!$this->retryAuthFailure) {
break;
}

if (!$this->io->isInteractive()) {
$message = "The '" . $this->fileUrl . "' URL required authentication.\nYou must be using the interactive console";

Expand All @@ -281,12 +307,7 @@ protected function callbackGet($notificationCode, $severity, $message, $messageC
$this->promptAuthAndRetry();
break;
}

if ($notificationCode === STREAM_NOTIFY_AUTH_REQUIRED) {
break;
}

throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', $messageCode);
break;

case STREAM_NOTIFY_AUTH_RESULT:
if (403 === $messageCode) {
Expand Down
7 changes: 6 additions & 1 deletion tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,14 @@ public function testPrivateRepository()
$remoteFilesystem->expects($this->at(1))
->method('getContents')
->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/authorizations'), $this->equalTo(false))
->will($this->returnValue('{"token": "abcdef"}'));
->will($this->returnValue('[]'));

$remoteFilesystem->expects($this->at(2))
->method('getContents')
->with($this->equalTo('github.com'), $this->equalTo('https://api.github.com/authorizations'), $this->equalTo(false))
->will($this->returnValue('{"token": "abcdef"}'));

$remoteFilesystem->expects($this->at(3))
->method('getContents')
->with($this->equalTo('github.com'), $this->equalTo($repoApiUrl), $this->equalTo(false))
->will($this->returnValue('{"master_branch": "test_master", "private": true}'));
Expand Down
11 changes: 2 additions & 9 deletions tests/Composer/Test/Util/RemoteFilesystemTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,11 @@ public function testCallbackGetNotifyProgress()
$this->assertAttributeEquals(50, 'lastProgress', $fs);
}

public function testCallbackGetNotifyFailure404()
public function testCallbackGetPassesThrough404()
{
$fs = new RemoteFilesystem($this->getMock('Composer\IO\IOInterface'));

try {
$this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0);
$this->fail();
} catch (\Exception $e) {
$this->assertInstanceOf('Composer\Downloader\TransportException', $e);
$this->assertEquals(404, $e->getCode());
$this->assertContains('HTTP/1.1 404 Not Found', $e->getMessage());
}
$this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0));
}

public function testCaptureAuthenticationParamsFromUrl()
Expand Down

0 comments on commit 81b86ac

Please sign in to comment.