PageRenderTime 74ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/Util/AuthHelper.php

http://github.com/composer/composer
PHP | 264 lines | 191 code | 27 blank | 46 comment | 52 complexity | 46bb43ccb9b6ea5241c426c483cf8f63 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Util;
  12. use Composer\Config;
  13. use Composer\IO\IOInterface;
  14. use Composer\Downloader\TransportException;
  15. /**
  16. * @author Jordi Boggiano <j.boggiano@seld.be>
  17. */
  18. class AuthHelper
  19. {
  20. protected $io;
  21. protected $config;
  22. private $displayedOriginAuthentications = array();
  23. public function __construct(IOInterface $io, Config $config)
  24. {
  25. $this->io = $io;
  26. $this->config = $config;
  27. }
  28. /**
  29. * @param string $origin
  30. * @param string|bool $storeAuth
  31. */
  32. public function storeAuth($origin, $storeAuth)
  33. {
  34. $store = false;
  35. $configSource = $this->config->getAuthConfigSource();
  36. if ($storeAuth === true) {
  37. $store = $configSource;
  38. } elseif ($storeAuth === 'prompt') {
  39. $answer = $this->io->askAndValidate(
  40. 'Do you want to store credentials for '.$origin.' in '.$configSource->getName().' ? [Yn] ',
  41. function ($value) {
  42. $input = strtolower(substr(trim($value), 0, 1));
  43. if (in_array($input, array('y','n'))) {
  44. return $input;
  45. }
  46. throw new \RuntimeException('Please answer (y)es or (n)o');
  47. },
  48. null,
  49. 'y'
  50. );
  51. if ($answer === 'y') {
  52. $store = $configSource;
  53. }
  54. }
  55. if ($store) {
  56. $store->addConfigSetting(
  57. 'http-basic.'.$origin,
  58. $this->io->getAuthentication($origin)
  59. );
  60. }
  61. }
  62. /**
  63. * @param string $url
  64. * @param string $origin
  65. * @param int $statusCode HTTP status code that triggered this call
  66. * @param string|null $reason a message/description explaining why this was called
  67. * @param string[] $headers
  68. * @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be
  69. * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json
  70. */
  71. public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $headers = array())
  72. {
  73. $storeAuth = false;
  74. $retry = false;
  75. if (in_array($origin, $this->config->get('github-domains'), true)) {
  76. $gitHubUtil = new GitHub($this->io, $this->config, null);
  77. $message = "\n";
  78. $rateLimited = $gitHubUtil->isRateLimited($headers);
  79. if ($rateLimited) {
  80. $rateLimit = $gitHubUtil->getRateLimit($headers);
  81. if ($this->io->hasAuthentication($origin)) {
  82. $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.';
  83. } else {
  84. $message = 'Create a GitHub OAuth token to go over the API rate limit.';
  85. }
  86. $message = sprintf(
  87. 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$url.'. '.$message.' You can also wait until %s for the rate limit to reset.',
  88. $rateLimit['limit'],
  89. $rateLimit['reset']
  90. )."\n";
  91. } else {
  92. $message .= 'Could not fetch '.$url.', please ';
  93. if ($this->io->hasAuthentication($origin)) {
  94. $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos';
  95. } else {
  96. $message .= 'create a GitHub OAuth token to access private repos';
  97. }
  98. }
  99. if (!$gitHubUtil->authorizeOAuth($origin)
  100. && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($origin, $message))
  101. ) {
  102. throw new TransportException('Could not authenticate against '.$origin, 401);
  103. }
  104. } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) {
  105. $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit');
  106. $gitLabUtil = new GitLab($this->io, $this->config, null);
  107. if ($this->io->hasAuthentication($origin) && ($auth = $this->io->getAuthentication($origin)) && in_array($auth['password'], array('gitlab-ci-token', 'private-token', 'oauth2'), true)) {
  108. throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode);
  109. }
  110. if (!$gitLabUtil->authorizeOAuth($origin)
  111. && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively(parse_url($url, PHP_URL_SCHEME), $origin, $message))
  112. ) {
  113. throw new TransportException('Could not authenticate against '.$origin, 401);
  114. }
  115. } elseif ($origin === 'bitbucket.org') {
  116. $askForOAuthToken = true;
  117. if ($this->io->hasAuthentication($origin)) {
  118. $auth = $this->io->getAuthentication($origin);
  119. if ($auth['username'] !== 'x-token-auth') {
  120. $bitbucketUtil = new Bitbucket($this->io, $this->config);
  121. $accessToken = $bitbucketUtil->requestToken($origin, $auth['username'], $auth['password']);
  122. if (!empty($accessToken)) {
  123. $this->io->setAuthentication($origin, 'x-token-auth', $accessToken);
  124. $askForOAuthToken = false;
  125. }
  126. } else {
  127. throw new TransportException('Could not authenticate against ' . $origin, 401);
  128. }
  129. }
  130. if ($askForOAuthToken) {
  131. $message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($statusCode === 401 || $statusCode === 403) ? 'access private repos' : 'go over the API rate limit');
  132. $bitBucketUtil = new Bitbucket($this->io, $this->config);
  133. if (! $bitBucketUtil->authorizeOAuth($origin)
  134. && (! $this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($origin, $message))
  135. ) {
  136. throw new TransportException('Could not authenticate against ' . $origin, 401);
  137. }
  138. }
  139. } else {
  140. // 404s are only handled for github
  141. if ($statusCode === 404) {
  142. return;
  143. }
  144. // fail if the console is not interactive
  145. if (!$this->io->isInteractive()) {
  146. if ($statusCode === 401) {
  147. $message = "The '" . $url . "' URL required authentication.\nYou must be using the interactive console to authenticate";
  148. } elseif ($statusCode === 403) {
  149. $message = "The '" . $url . "' URL could not be accessed: " . $reason;
  150. } else {
  151. $message = "Unknown error code '" . $statusCode . "', reason: " . $reason;
  152. }
  153. throw new TransportException($message, $statusCode);
  154. }
  155. // fail if we already have auth
  156. if ($this->io->hasAuthentication($origin)) {
  157. throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode);
  158. }
  159. $this->io->writeError(' Authentication required (<info>'.$origin.'</info>):');
  160. $username = $this->io->ask(' Username: ');
  161. $password = $this->io->askAndHideAnswer(' Password: ');
  162. $this->io->setAuthentication($origin, $username, $password);
  163. $storeAuth = $this->config->get('store-auths');
  164. }
  165. $retry = true;
  166. return array('retry' => $retry, 'storeAuth' => $storeAuth);
  167. }
  168. /**
  169. * @param array $headers
  170. * @param string $origin
  171. * @param string $url
  172. * @return array updated headers array
  173. */
  174. public function addAuthenticationHeader(array $headers, $origin, $url)
  175. {
  176. if ($this->io->hasAuthentication($origin)) {
  177. $authenticationDisplayMessage = null;
  178. $auth = $this->io->getAuthentication($origin);
  179. if ($auth['password'] === 'bearer') {
  180. $headers[] = 'Authorization: Bearer '.$auth['username'];
  181. } elseif ('github.com' === $origin && 'x-oauth-basic' === $auth['password']) {
  182. // only add the access_token if it is actually a github API URL
  183. if (preg_match('{^https?://api\.github\.com/}', $url)) {
  184. $headers[] = 'Authorization: token '.$auth['username'];
  185. $authenticationDisplayMessage = 'Using GitHub token authentication';
  186. }
  187. } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) {
  188. if ($auth['password'] === 'oauth2') {
  189. $headers[] = 'Authorization: Bearer '.$auth['username'];
  190. $authenticationDisplayMessage = 'Using GitLab OAuth token authentication';
  191. } elseif ($auth['password'] === 'private-token' || $auth['password'] === 'gitlab-ci-token') {
  192. $headers[] = 'PRIVATE-TOKEN: '.$auth['username'];
  193. $authenticationDisplayMessage = 'Using GitLab private token authentication';
  194. }
  195. } elseif (
  196. 'bitbucket.org' === $origin
  197. && $url !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL
  198. && 'x-token-auth' === $auth['username']
  199. ) {
  200. if (!$this->isPublicBitBucketDownload($url)) {
  201. $headers[] = 'Authorization: Bearer ' . $auth['password'];
  202. $authenticationDisplayMessage = 'Using Bitbucket OAuth token authentication';
  203. }
  204. } else {
  205. $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
  206. $headers[] = 'Authorization: Basic '.$authStr;
  207. $authenticationDisplayMessage = 'Using HTTP basic authentication with username "' . $auth['username'] . '"';
  208. }
  209. if ($authenticationDisplayMessage && !in_array($origin, $this->displayedOriginAuthentications, true)) {
  210. $this->io->writeError($authenticationDisplayMessage, true, IOInterface::DEBUG);
  211. $this->displayedOriginAuthentications[] = $origin;
  212. }
  213. }
  214. return $headers;
  215. }
  216. /**
  217. * @link https://github.com/composer/composer/issues/5584
  218. *
  219. * @param string $urlToBitBucketFile URL to a file at bitbucket.org.
  220. *
  221. * @return bool Whether the given URL is a public BitBucket download which requires no authentication.
  222. */
  223. public function isPublicBitBucketDownload($urlToBitBucketFile)
  224. {
  225. $domain = parse_url($urlToBitBucketFile, PHP_URL_HOST);
  226. if (strpos($domain, 'bitbucket.org') === false) {
  227. // Bitbucket downloads are hosted on amazonaws.
  228. // We do not need to authenticate there at all
  229. return true;
  230. }
  231. $path = parse_url($urlToBitBucketFile, PHP_URL_PATH);
  232. // Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever}
  233. // {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/}
  234. $pathParts = explode('/', $path);
  235. return count($pathParts) >= 4 && $pathParts[3] == 'downloads';
  236. }
  237. }