From fb5e50e6cc1f9de948dbd641349f84093f3332de Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 27 Jan 2015 14:51:09 -0800 Subject: [PATCH] Proxy VCS HTTP requests Summary: Ref T7019. When we receive a `git clone https://` (or `git push` on HTTP/S), and the repository is not local, proxy the request to the appropriate service. This has scalability limits, but they are not more severe than the existing limits (T4369) and are about as abstracted as we can get them. This doesn't fully work in a Phacility context because the commit hook does not know which instance it is running in, but that problem is not unique to HTTP. Test Plan: - Pushed and pulled a Git repo via proxy. - Pulled a Git repo normally. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T7019 Differential Revision: https://secure.phabricator.com/D11494 --- src/aphront/AphrontRequest.php | 132 +++++++++++++++++- .../controller/DiffusionServeController.php | 40 +++++- 2 files changed, 166 insertions(+), 6 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 6e167c64e5..3f7036b092 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -1,9 +1,9 @@ getDomain(); + $ip = gethostbyname($domain); + if (!$ip) { + throw new Exception( + pht( + 'Unable to resolve domain "%s"!', + $domain)); + } + + if (!PhabricatorEnv::isClusterAddress($ip)) { + throw new Exception( + pht( + 'Refusing to proxy a request to IP address ("%s") which is not '. + 'in the cluster address block (this address was derived by '. + 'resolving the domain "%s").', + $ip, + $domain)); + } + + $uri->setPath($this->getPath()); + $uri->setQueryParams(self::flattenData($_GET)); + + $input = PhabricatorStartup::getRawInput(); + + $future = id(new HTTPSFuture($uri)) + ->addHeader('Host', self::getHost()) + ->addHeader('X-Phabricator-Cluster', true) + ->setMethod($_SERVER['REQUEST_METHOD']) + ->write($input); + + if (isset($_SERVER['PHP_AUTH_USER'])) { + $future->setHTTPBasicAuthCredentials( + $_SERVER['PHP_AUTH_USER'], + new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', ''))); + } + + $headers = array(); + $seen = array(); + + // NOTE: apache_request_headers() might provide a nicer way to do this, + // but isn't available under FCGI until PHP 5.4.0. + foreach ($_SERVER as $key => $value) { + if (preg_match('/^HTTP_/', $key)) { + // Unmangle the header as best we can. + $key = str_replace('_', ' ', $key); + $key = strtolower($key); + $key = ucwords($key); + $key = str_replace(' ', '-', $key); + + $headers[] = array($key, $value); + $seen[$key] = true; + } + } + + // In some situations, this may not be mapped into the HTTP_X constants. + // CONTENT_LENGTH is similarly affected, but we trust cURL to take care + // of that if it matters, since we're handing off a request body. + if (empty($seen['Content-Type'])) { + if (isset($_SERVER['CONTENT_TYPE'])) { + $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']); + } + } + + foreach ($headers as $header) { + list($key, $value) = $header; + switch ($key) { + case 'Host': + case 'Authorization': + // Don't forward these headers, we've already handled them elsewhere. + unset($headers[$key]); + break; + default: + break; + } + } + + foreach ($headers as $header) { + list($key, $value) = $header; + $future->addHeader($key, $value); + } + + return $future; + } + + } diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 27d256e2a8..2b82bde43d 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -205,10 +205,8 @@ final class DiffusionServeController extends DiffusionController { } else { switch ($vcs_type) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - $result = $this->serveGitRequest($repository, $viewer); - break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: - $result = $this->serveMercurialRequest($repository, $viewer); + $result = $this->serveVCSRequest($repository, $viewer); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $result = new PhabricatorVCSResponse( @@ -238,6 +236,42 @@ final class DiffusionServeController extends DiffusionController { return $result; } + private function serveVCSRequest( + PhabricatorRepository $repository, + PhabricatorUser $viewer) { + + // If this repository is hosted on a service, we need to proxy the request + // to a host which can serve it. + $is_cluster_request = $this->getRequest()->isProxiedClusterRequest(); + + $uri = $repository->getAlmanacServiceURI( + $viewer, + $is_cluster_request, + array( + 'http', + 'https', + )); + if ($uri) { + $future = $this->getRequest()->newClusterProxyFuture($uri); + return id(new AphrontHTTPProxyResponse()) + ->setHTTPFuture($future); + } + + // Otherwise, we're going to handle the request locally. + + $vcs_type = $repository->getVersionControlSystem(); + switch ($vcs_type) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + $result = $this->serveGitRequest($repository, $viewer); + break; + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: + $result = $this->serveMercurialRequest($repository, $viewer); + break; + } + + return $result; + } + private function isReadOnlyRequest( PhabricatorRepository $repository) { $request = $this->getRequest();