From 23d35204cd28374701b79dc4190b4661b3f81c5f Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:06:29 +0000 Subject: [PATCH 01/21] Bail out of the normal 401 handling routine when the origin is GitHub --- src/Composer/Util/RemoteFilesystem.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3ecec03d..53829d03a 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -247,6 +247,11 @@ class RemoteFilesystem throw new TransportException($message, 401); } + // GitHub requests bail out early to allow 2FA to be applied if requested. + if ('github.com' === $this->originUrl) { + throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401); + } + $this->promptAuthAndRetry(); break; } From 3f53acc9af3a998dbddbdf59c9cc52053c9ae29d Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:06:58 +0000 Subject: [PATCH 02/21] Test if the 401 was caused by 2FA and ask for OTP if appropriate --- src/Composer/Util/RemoteFilesystem.php | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 53829d03a..158ff8034 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -145,6 +145,42 @@ class RemoteFilesystem if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); } + + // 401 when authentication was supplied, handle 2FA if required. + if ($e instanceof TransportException && 401 === $e->getCode() && $this->io->hasAuthentication($this->originUrl)) { + $headerNames = array_map(function($header) { + return strstr($header, ':', true); + }, $e->getHeaders()); + + if ($key = array_search('X-GitHub-OTP', $headerNames)) { + $headers = $e->getHeaders(); + list($required, $method) = explode(';', trim(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) { + // @todo + } + + $this->options['github-otp'] = trim($this->io->ask('Authentication Code: ')); + + $this->retry = true; + } + } else { + try { + $this->promptAuthAndRetry(); + } catch (TransportException $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.')'; From 360df90ba59a29c1ad03c75370a46327441a5664 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:07:26 +0000 Subject: [PATCH 03/21] Add GitHub OTP to request headers --- src/Composer/Util/RemoteFilesystem.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 158ff8034..26b2774c9 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -378,6 +378,13 @@ class RemoteFilesystem } } + // Handle GitHub two factor tokens. + if (isset($options['github-otp'])) { + $headers[] = 'X-GitHub-OTP: ' . $options['github-otp']; + + unset($options['github-otp']); + } + if (isset($options['http']['header']) && !is_array($options['http']['header'])) { $options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n")); } From 9a0f4392da69f05b09292c2f18f86f5ea86d1e29 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 10 Jan 2014 16:11:45 +0000 Subject: [PATCH 04/21] Trim whitepsace from each argument --- src/Composer/Util/RemoteFilesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 26b2774c9..890bd5aad 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -154,7 +154,7 @@ class RemoteFilesystem if ($key = array_search('X-GitHub-OTP', $headerNames)) { $headers = $e->getHeaders(); - list($required, $method) = explode(';', trim(substr(strstr($headers[$key], ':'), 1))); + list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1))); if ('required' === $required) { $this->io->write('Two-factor Authentication'); From 20dac3e836a02b57794559bb0501c356cd140459 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:31:56 +0000 Subject: [PATCH 05/21] Remove GitHub OTP code from RFS class --- src/Composer/Util/RemoteFilesystem.php | 48 -------------------------- 1 file changed, 48 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 890bd5aad..d3ecec03d 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -145,42 +145,6 @@ class RemoteFilesystem if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); } - - // 401 when authentication was supplied, handle 2FA if required. - if ($e instanceof TransportException && 401 === $e->getCode() && $this->io->hasAuthentication($this->originUrl)) { - $headerNames = array_map(function($header) { - return 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) { - // @todo - } - - $this->options['github-otp'] = trim($this->io->ask('Authentication Code: ')); - - $this->retry = true; - } - } else { - try { - $this->promptAuthAndRetry(); - } catch (TransportException $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.')'; @@ -283,11 +247,6 @@ class RemoteFilesystem throw new TransportException($message, 401); } - // GitHub requests bail out early to allow 2FA to be applied if requested. - if ('github.com' === $this->originUrl) { - throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401); - } - $this->promptAuthAndRetry(); break; } @@ -378,13 +337,6 @@ class RemoteFilesystem } } - // Handle GitHub two factor tokens. - if (isset($options['github-otp'])) { - $headers[] = 'X-GitHub-OTP: ' . $options['github-otp']; - - unset($options['github-otp']); - } - if (isset($options['http']['header']) && !is_array($options['http']['header'])) { $options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n")); } From 3f6a62099dd01e728b3215f968645d0c8d2237e7 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:32:55 +0000 Subject: [PATCH 06/21] Add an option which causes reauth attempts to be bypassed --- src/Composer/Util/RemoteFilesystem.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3ecec03d..67b429837 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -33,6 +33,7 @@ class RemoteFilesystem private $progress; private $lastProgress; private $options; + private $retryAuthFailure; /** * Constructor. @@ -109,12 +110,19 @@ class RemoteFilesystem $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); if ($this->io->isDebug()) { @@ -247,6 +255,11 @@ class RemoteFilesystem throw new TransportException($message, 401); } + // Bail if the caller is going to handle authentication failures itself. + if (!$this->retryAuthFailure) { + throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401); + } + $this->promptAuthAndRetry(); break; } From be5e4b1589f74edc7414f1c8ed63b96ef9205204 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:37:07 +0000 Subject: [PATCH 07/21] Intercept auth rejections requiring an OTP token --- src/Composer/Util/GitHub.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 49e56f8c9..affae8c64 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -111,6 +111,34 @@ class GitHub ))); } 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 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) { + // @todo + } + + $otp = $this->io->ask('Authentication Code: '); + + continue; + } + } + } + $this->io->write('Invalid credentials.'); continue; } From 7e0d8c1bc5e098209cb7ee4f236c9962b897e3bb Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:38:14 +0000 Subject: [PATCH 08/21] Do not ask for credentials again if OTP token is present --- src/Composer/Util/GitHub.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index affae8c64..e3185745d 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -87,9 +87,13 @@ class GitHub $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'; From cedae88b67e5a3940063536600d9bda20cf38f8c Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:38:43 +0000 Subject: [PATCH 09/21] Add OTP token to the request headers --- src/Composer/Util/GitHub.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index e3185745d..41fe63740 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -101,11 +101,17 @@ class GitHub $appName .= ' on ' . trim($output); } + $headers = array('Content-Type: application/json'); + + if ($otp) { + $headers[] = 'X-GitHub-OTP: ' . $otp; + } + $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( 'http' => array( 'method' => 'POST', 'follow_location' => false, - 'header' => "Content-Type: application/json\r\n", + 'header' => $headers, 'content' => json_encode(array( 'scopes' => array('repo'), 'note' => $appName, From 2a08f55079d85529277840e6d99d318319cf11e6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 17 Jan 2014 15:39:05 +0000 Subject: [PATCH 10/21] Bypass RFS auth failure handling --- src/Composer/Util/GitHub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 41fe63740..6794dd507 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -108,6 +108,7 @@ class GitHub } $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( + 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', 'follow_location' => false, From bcee7a04ee54e2d9b232a702c379eb0389255656 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:26:43 +0000 Subject: [PATCH 11/21] Add message when SMS authentication code is required --- src/Composer/Util/GitHub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 6794dd507..40060395f 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -140,7 +140,7 @@ class GitHub } if ('sms' === $method) { - // @todo + $this->io->write('You have been sent an SMS message with an authentication code to verify your identity.'); } $otp = $this->io->ask('Authentication Code: '); From f1af43068ce6e04f061d0593aaea8608d1432ae6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:33:13 +0000 Subject: [PATCH 12/21] Change docs to reflect support for GitHub 2FA --- doc/04-schema.md | 2 +- doc/articles/troubleshooting.md | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 96641e8fe..635136e2b 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -684,7 +684,7 @@ 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) + [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. diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index 2fc7bb487..a0b4565b6 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -105,12 +105,13 @@ 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: + +If you would rather than 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. From 0858e96ac685dcf6be98cb544cc36bac5e3d9741 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:33:47 +0000 Subject: [PATCH 13/21] Correct capitalisation of OAuth --- doc/04-schema.md | 2 +- doc/articles/troubleshooting.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/04-schema.md b/doc/04-schema.md index 635136e2b..c48955ee6 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -685,7 +685,7 @@ The following options are supported: 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-oauth-tokens) - on how to get an oauth token for GitHub. + 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 diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index a0b4565b6..aa11715fc 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -105,7 +105,7 @@ 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 oauth tokens +## 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. @@ -113,7 +113,7 @@ for authentication asking your username and password so it can go ahead with its If you would rather than 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. +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 ` From 78568b49d6cce8d282fd8d199650c0afde565c06 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:36:00 +0000 Subject: [PATCH 14/21] Correct use of English --- doc/articles/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index aa11715fc..d3637cd03 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -110,7 +110,7 @@ Or, you can increase the limit with a command-line argument: 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. -If you would rather than provide your GitHub credentials to Composer you can +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. From 8b7cdb7fb417f349eab04335d0a7bc3956343a81 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 31 Jan 2014 16:42:49 +0000 Subject: [PATCH 15/21] Treat HTTP header as case insensitive --- src/Composer/Util/GitHub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 40060395f..942e7749c 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -125,10 +125,10 @@ class GitHub // 401 when authentication was supplied, handle 2FA if required. if ($this->io->hasAuthentication($originUrl)) { $headerNames = array_map(function($header) { - return strstr($header, ':', true); + return strtolower(strstr($header, ':', true)); }, $e->getHeaders()); - if ($key = array_search('X-GitHub-OTP', $headerNames)) { + if ($key = array_search('x-github-otp', $headerNames)) { $headers = $e->getHeaders(); list($required, $method) = array_map('trim', explode(';', substr(strstr($headers[$key], ':'), 1))); From e1e48b28f75e9c6a293fc032588093c6f6d8d3fb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 26 Feb 2014 10:43:26 +0100 Subject: [PATCH 16/21] Update vendor dir modified time after every install/update, fixes #2764 --- src/Composer/Installer.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 4e4502dfe..d373beb43 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -297,6 +297,11 @@ class Installer $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; From 28bb78132445cbccbffac373a3bfa3b974eb876f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 26 Feb 2014 15:51:06 +0100 Subject: [PATCH 17/21] Capture response bodies in exceptions when http requests fail --- .../Downloader/TransportException.php | 11 +++++++++ src/Composer/Util/RemoteFilesystem.php | 24 ++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Composer/Downloader/TransportException.php b/src/Composer/Downloader/TransportException.php index b28f8d470..2e4b42f01 100644 --- a/src/Composer/Downloader/TransportException.php +++ b/src/Composer/Downloader/TransportException.php @@ -18,6 +18,7 @@ namespace Composer\Downloader; class TransportException extends \RuntimeException { protected $headers; + protected $response; public function setHeaders($headers) { @@ -28,4 +29,14 @@ class TransportException extends \RuntimeException { return $this->headers; } + + public function setResponse($response) + { + $this->response = $response; + } + + public function getResponse() + { + return $this->response; + } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index d3ecec03d..68c76f341 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -124,6 +124,9 @@ class RemoteFilesystem $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) { @@ -145,6 +148,10 @@ class RemoteFilesystem 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.')'; @@ -154,10 +161,16 @@ class RemoteFilesystem 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 @@ -250,12 +263,7 @@ class RemoteFilesystem $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) { From 5067d76dbc60f26992da4402c3f21af91bd4b57a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 26 Feb 2014 16:01:31 +0100 Subject: [PATCH 18/21] Adjust test suite --- tests/Composer/Test/Util/RemoteFilesystemTest.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index eabfe9ed5..92aa60630 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -130,18 +130,11 @@ class RemoteFilesystemTest extends \PHPUnit_Framework_TestCase $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() From 1851c29dd3d2d05096d3e1d2166fc0d73aa622eb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 26 Feb 2014 17:19:54 +0100 Subject: [PATCH 19/21] Update code to work with #2766 --- src/Composer/Util/RemoteFilesystem.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 7c7957ca7..fd5d431c6 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -262,17 +262,17 @@ class RemoteFilesystem 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"; throw new TransportException($message, 401); } - // Bail if the caller is going to handle authentication failures itself. - if (!$this->retryAuthFailure) { - throw new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.trim($message).')', 401); - } - $this->promptAuthAndRetry(); break; } From 5b0dc99fec6d2233db57d97cac3dd15824ea21f7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 26 Feb 2014 17:20:47 +0100 Subject: [PATCH 20/21] Reuse github existing tokens instead of failing, fixes #2724 --- src/Composer/Util/GitHub.php | 50 +++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 942e7749c..b69073d13 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -101,25 +101,51 @@ class GitHub $appName .= ' on ' . trim($output); } - $headers = array('Content-Type: application/json'); - + $headers = array(); if ($otp) { - $headers[] = 'X-GitHub-OTP: ' . $otp; + $headers = array('X-GitHub-OTP: ' . $otp); } - $contents = JsonFile::parseJson($this->remoteFilesystem->getContents($originUrl, 'https://'. $apiUrl . '/authorizations', false, array( + // 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' => $headers, - '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. From 0d4c2bb7d7a864a9b3e876908e743310cdeaa5e6 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 26 Feb 2014 17:38:58 +0100 Subject: [PATCH 21/21] Fix github test --- tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index 906c20071..f452f224d 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -81,9 +81,14 @@ class GitHubDriverTest extends \PHPUnit_Framework_TestCase $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}'));