Эх сурвалжийг харах

Merge branch 'pr-1' into gitlab

* pr-1:
  Gitlab:Use proper fallbacks if archive download is failing
  Remove two factor authentication
  Remove parasite
  Remove composer.phar
  Compiler.phar update
  Add oauth2 support for gitlab
  Add compiled composer.phar
Jerome TAMARELLE 10 жил өмнө
parent
commit
6ec9f5ff02

+ 2 - 2
src/Composer/IO/BaseIO.php

@@ -69,9 +69,9 @@ abstract class BaseIO implements IOInterface
             }
         }
 
-        if ($tokens = $config->get('gitlab-tokens')) {
+        if ($tokens = $config->get('gitlab-oauth')) {
             foreach ($tokens as $domain => $token) {
-                $this->setAuthentication($domain, $token, 'gitlab-private-token');
+                $this->setAuthentication($domain, $token, 'oauth2');
             }
         }
 

+ 85 - 2
src/Composer/Repository/Vcs/GitLabDriver.php

@@ -18,7 +18,7 @@ use Composer\IO\IOInterface;
 use Composer\Json\JsonFile;
 use Composer\Downloader\TransportException;
 use Composer\Util\RemoteFilesystem;
-
+use Composer\Util\GitLab;
 /**
  * Driver for GitLab API, use the Git driver for local checkouts.
  *
@@ -54,6 +54,13 @@ class GitLabDriver extends VcsDriver
      */
     private $branches;
 
+    /**
+     * Git Driver
+     *
+     * @var GitDriver
+     */
+    protected $gitDriver;
+
     /**
      * Extracts information from the repository url.
      * SSH urls are not supported in order to know the HTTP sheme to use.
@@ -248,10 +255,86 @@ class GitLabDriver extends VcsDriver
     {
         // we need to fetch the default branch from the api
         $resource = $this->getApiUrl();
+        $this->project = JsonFile::parseJson($this->getContents($resource, true), $resource);
+    }
+
+    protected function attemptCloneFallback()
+    {
 
-        $this->project = JsonFile::parseJson($this->getContents($resource), $resource);
+        try {
+            // If this repository may be private and we
+            // cannot ask for authentication credentials (because we
+            // are not interactive) then we fallback to GitDriver.
+            $this->setupGitDriver($this->generateSshUrl());
+
+            return;
+        } catch (\RuntimeException $e) {
+            $this->gitDriver = null;
+
+            $this->io->writeError('<error>Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your credentials</error>');
+            throw $e;
+        }
     }
 
+    protected function setupGitDriver($url)
+    {
+        $this->gitDriver = new GitDriver(
+            array('url' => $url),
+            $this->io,
+            $this->config,
+            $this->process,
+            $this->remoteFilesystem
+        );
+        $this->gitDriver->initialize();
+    }    
+
+    /**
+     * {@inheritDoc}
+     */
+    protected function getContents($url, $fetchingRepoData = false)
+    {
+        try {
+            return parent::getContents($url);
+        } catch (TransportException $e) {
+            $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->remoteFilesystem);
+
+            switch ($e->getCode()) {
+                case 401:
+                case 404:
+                    // try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404
+                    if (!$fetchingRepoData) {
+                        throw $e;
+                    }
+
+                    if ($gitLabUtil->authorizeOAuth($this->originUrl)) {
+                        return parent::getContents($url);
+                    }
+
+                    if (!$this->io->isInteractive()) {
+                        return $this->attemptCloneFallback();
+                    }
+                    $this->io->writeError('<warning>Failed to download ' . $this->owner . '/' . $this->repository . ':' . $e->getMessage() . '</warning>');
+                    $gitLabUtil->authorizeOAuthInteractively($this->originUrl, 'Your credentials are required to fetch private repository metadata (<info>'.$this->url.'</info>)');
+
+                    return parent::getContents($url);
+
+                case 403:
+                    if (!$this->io->hasAuthentication($this->originUrl) && $gitLabUtil->authorizeOAuth($this->originUrl)) {
+                        return parent::getContents($url);
+                    }
+
+                    if (!$this->io->isInteractive() && $fetchingRepoData) {
+                        return $this->attemptCloneFallback();
+                    }
+
+                    throw $e;
+
+                default:
+                    throw $e;
+            }
+        }
+    }    
+
     /**
      * Uses the config `gitlab-domains` to see if the driver supports the url for the
      * repository given.

+ 28 - 3
src/Composer/Util/Git.php

@@ -80,13 +80,16 @@ class Git
             $this->throwException('Failed to clone ' . self::sanitizeUrl($url) .' via '.implode(', ', $protocols).' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url);
         }
 
-        // if we have a private github url and the ssh protocol is disabled then we skip it and directly fallback to https
+        // if we have a private github/gitlab url and the ssh protocol is disabled then we skip it and directly fallback to https
         $bypassSshForGitHub = preg_match('{^git@'.self::getGitHubDomainsRegex($this->config).':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true);
+        $bypassSshForGitLab = preg_match('{^git@'.self::getGitLabDomainsRegex($this->config).':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true);
 
         $command = call_user_func($commandCallable, $url);
-        if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $ignoredOutput, $cwd)) {
-            // private github repository without git access, try https with auth
+
+        if ($bypassSshForGitHub || $bypassSshForGitLab || 0 !== $this->process->execute($command, $ignoredOutput, $cwd)) {
+            // private github/gitlab repository without git access, try https with auth
             if (preg_match('{^git@'.self::getGitHubDomainsRegex($this->config).':(.+?)\.git$}i', $url, $match)) {
+   
                 if (!$this->io->hasAuthentication($match[1])) {
                     $gitHubUtil = new GitHub($this->io, $this->config, $this->process);
                     $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos';
@@ -99,7 +102,24 @@ class Git
                 if ($this->io->hasAuthentication($match[1])) {
                     $auth = $this->io->getAuthentication($match[1]);
                     $url = 'https://'.rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@'.$match[1].'/'.$match[2].'.git';
+                    $command = call_user_func($commandCallable, $url);
+                    if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
+                        return;
+                    }
+                }
+            } elseif (preg_match('{^git@'.self::getGitLabDomainsRegex($this->config).':(.+?)\.git$}i', $url, $match)) {
+                if (!$this->io->hasAuthentication($match[1])) {
+                    $gitLabUtil = new GitLab($this->io, $this->config, $this->process);
+                    $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos';
 
+                    if (!$gitLabUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) {
+                        $gitLabUtil->authorizeOAuthInteractively($match[1], $message);
+                    }
+                }
+
+                if ($this->io->hasAuthentication($match[1])) {
+                    $auth = $this->io->getAuthentication($match[1]);
+                    $url = 'http://oauth2:' . rawurlencode($auth['username']) . '@'.$match[1].'/'.$match[2].'.git';
                     $command = call_user_func($commandCallable, $url);
                     if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) {
                         return;
@@ -183,6 +203,11 @@ class Git
         return '('.implode('|', array_map('preg_quote', $config->get('github-domains'))).')';
     }
 
+    public static function getGitLabDomainsRegex(Config $config)
+    {
+        return '('.implode('|', array_map('preg_quote', $config->get('gitlab-domains'))).')';
+    }
+
     public static function sanitizeUrl($message)
     {
         return preg_replace('{://([^@]+?):.+?@}', '://$1:***@', $message);

+ 163 - 0
src/Composer/Util/GitLab.php

@@ -0,0 +1,163 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Roshan Gautam <roshan.gautam@hotmail.com>
+ *     
+ *
+ * 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 Roshan Gautam <roshan.gautam@hotmail.com>
+ */
+class GitLab
+{
+    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, $config);
+    }
+
+    /**
+     * Attempts to authorize a GitLab domain via OAuth
+     *
+     * @param  string $originUrl The host this GitLab instance is located at
+     * @return bool   true on success
+     */
+    public function authorizeOAuth($originUrl)
+    {
+        if (!in_array($originUrl, $this->config->get('gitlab-domains'))) {
+            return false;
+        }
+
+        // if available use token from git config
+        if (0 === $this->process->execute('git config gitlab.accesstoken', $output)) {
+            $this->io->setAuthentication($originUrl, trim($output), 'oauth2');
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Authorizes a GitLab domain interactively via OAuth
+     *
+     * @param  string                        $originUrl The host this GitLab instance is located at
+     * @param  string                        $message   The reason this authorization is required
+     * @throws \RuntimeException
+     * @throws TransportException|\Exception
+     * @return bool                          true on success
+     */
+    public function authorizeOAuthInteractively($originUrl, $message = null)
+    {
+        if ($message) {
+            $this->io->writeError($message);
+        }
+
+
+        $this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName()));
+        $this->io->writeError('To revoke access to this token you can visit ' . $this->config->get('gitlab-domains')[0] . '/profile/applications');
+
+        $attemptCounter = 0;
+
+        while ($attemptCounter++ < 5) {
+            try {
+                $response = $this->createToken($originUrl);
+            } catch (TransportException $e) {
+                // 401 is bad credentials, 
+                // 403 is max login attempts exceeded
+                if (in_array($e->getCode(), array(403, 401))) {
+
+                    if (401 === $e->getCode()) {
+                        $this->io->writeError('Bad credentials.');
+                    } else {
+                        $this->io->writeError('Maximum number of login attempts exceeded. Please try again later.');
+                    }
+
+                    $this->io->writeError('You can also manually create a personal token at ' . $this->config->get('gitlab-domains')[0] . '/profile/applications');
+                    $this->io->writeError('Add it using "composer config gitlab-oauth.' . $this->config->get('gitlab-domains')[0] . ' <token>"');
+
+                    continue;
+                }
+
+                throw $e;
+            }
+
+            $this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2');
+            $this->config->getConfigSource()->removeConfigSetting('gitlab-oauth.'.$originUrl);
+            // store value in user config
+            $this->config->getAuthConfigSource()->addConfigSetting('gitlab-oauth.'.$originUrl, $response['access_token']);
+
+            return true;
+        }
+
+        throw new \RuntimeException("Invalid GitLab credentials 5 times in a row, aborting.");
+    }
+
+    private function createToken($originUrl)
+    {
+        if (!$this->io->hasAuthentication($originUrl)) {
+            $username = $this->io->ask('Username: ');
+            $password = $this->io->askAndHideAnswer('Password: ');
+
+            $this->io->setAuthentication($originUrl, $username, $password);
+        }
+
+
+        $headers = array('Content-Type: application/x-www-form-urlencoded');
+
+        $note = 'Composer';
+        if ($this->config->get('GitLab-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) {
+            $note .= ' on ' . trim($output);
+        }
+        $note .= ' [' . date('YmdHis') . ']';
+
+        $apiUrl = $originUrl ;
+        $data = http_build_query(
+            array(
+                'username'  => $username,
+                'password'  => $password,
+                'grant_type' => 'password',            
+                )
+            );
+        $options = array(
+            'retry-auth-failure' => false,
+            'http' => array(
+                'method' => 'POST',
+                'header' => $headers,
+                'content' => $data
+            ));
+
+        $json = $this->remoteFilesystem->getContents($originUrl, 'http://'. $apiUrl . '/oauth/token', false, $options);
+
+        $this->io->writeError('Token successfully created');
+
+        return JsonFile::parseJson($json);
+    }
+}

+ 26 - 10
src/Composer/Util/RemoteFilesystem.php

@@ -145,6 +145,12 @@ class RemoteFilesystem
 
         $options = $this->getOptionsForUrl($originUrl, $additionalOptions);
 
+        if (isset($options['retry-auth-failure'])) {
+            $this->retryAuthFailure = (bool) $options['retry-auth-failure'];
+
+            unset($options['retry-auth-failure']);
+        }
+
         if ($this->io->isDebug()) {
             $this->io->writeError((substr($fileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $fileUrl);
         }
@@ -154,15 +160,20 @@ class RemoteFilesystem
             unset($options['github-token']);
         }
 
+        if (isset($options['gitlab-token'])) {
+            $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token'];
+            unset($options['gitlab-token']);
+        }
+
         if (isset($options['http'])) {
             $options['http']['ignore_errors'] = true;
         }
+ 
         $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet')));
 
         if ($this->progress) {
             $this->io->writeError("    Downloading: <comment>connection...</comment>", false);
         }
-
         $errorMessage = '';
         $errorCode = 0;
         $result = false;
@@ -352,13 +363,12 @@ class RemoteFilesystem
                 throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
             }
         } else if ($this->config && in_array($this->originUrl, $this->config->get('gitlab-domains'), true)) {
-            if ($this->io->isInteractive()) {
-                $this->io->overwrite('Enter your GitLab private token to access API (<info>'.parse_url($this->fileUrl, PHP_URL_HOST).'</info>):');
-                $token = $this->io->askAndHideAnswer('      Private-Token: ');
-                $this->io->setAuthentication($this->originUrl, $token, 'gitlab-private-token');
-                $this->config->getAuthConfigSource()->addConfigSetting('gitlab-tokens.'.$this->originUrl, $token);
-            } else {
-                throw new TransportException("The GitLab URL requires authentication.\nYou must be using the interactive console to authenticate", $httpStatus);
+            $message = "\n".'Could not fetch '.$this->fileUrl.', enter your ' . $this->config->get('gitlab-domains')[0] . ' credentials ' .($httpStatus === 401 ? 'to access private repos' : 'to go over the API rate limit');
+            $gitLabUtil = new GitLab($this->io, $this->config, null);
+            if (!$gitLabUtil->authorizeOAuth($this->originUrl)
+                && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively($this->originUrl, $message))
+            ) {
+                throw new TransportException('Could not authenticate against '.$this->originUrl, 401);
             }
         } else {
             // 404s are only handled for github
@@ -417,12 +427,15 @@ class RemoteFilesystem
 
         $options = array_replace_recursive($this->options, $additionalOptions);
 
+
         if ($this->io->hasAuthentication($originUrl)) {
             $auth = $this->io->getAuthentication($originUrl);
             if ('github.com' === $originUrl && 'x-oauth-basic' === $auth['password']) {
                 $options['github-token'] = $auth['username'];
-            } elseif ($auth['password'] === 'gitlab-private-token') {
-                $headers[] = 'Private-Token: '.$auth['username'];
+            } elseif ($originUrl === $this->config->get('gitlab-domains')[0]) {
+                if($auth['password'] === 'oauth2') {
+                    $headers[] = 'Authorization: Bearer '.$auth['username'];    
+                }
             } else {
                 $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
                 $headers[] = 'Authorization: Basic '.$authStr;
@@ -436,6 +449,9 @@ class RemoteFilesystem
             $options['http']['header'][] = $header;
         }
 
+        if($this->config && $this->config->get('gitlab-domains') && $originUrl == $this->config->get('gitlab-domains')[0]) {
+            $options['retry-auth-failure'] = false;
+        }
         return $options;
     }
 }