Skip to content

Commit fb5e50e

Browse files
author
epriestley
committed
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
1 parent 51b2c4d commit fb5e50e

File tree

2 files changed

+166
-6
lines changed

2 files changed

+166
-6
lines changed

src/aphront/AphrontRequest.php

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?php
22

33
/**
4-
* @task data Accessing Request Data
5-
* @task cookie Managing Cookies
6-
*
4+
* @task data Accessing Request Data
5+
* @task cookie Managing Cookies
6+
* @task cluster Working With a Phabricator Cluster
77
*/
88
final class AphrontRequest {
99

@@ -625,4 +625,130 @@ public static function getHTTPHeader($name, $default = null, $data = null) {
625625
return $default;
626626
}
627627

628+
629+
/* -( Working With a Phabricator Cluster )--------------------------------- */
630+
631+
632+
/**
633+
* Is this a proxied request originating from within the Phabricator cluster?
634+
*
635+
* IMPORTANT: This means the request is dangerous!
636+
*
637+
* These requests are **more dangerous** than normal requests (they can not
638+
* be safely proxied, because proxying them may cause a loop). Cluster
639+
* requests are not guaranteed to come from a trusted source, and should
640+
* never be treated as safer than normal requests. They are strictly less
641+
* safe.
642+
*/
643+
public function isProxiedClusterRequest() {
644+
return (bool)AphrontRequest::getHTTPHeader('X-Phabricator-Cluster');
645+
}
646+
647+
648+
/**
649+
* Build a new @{class:HTTPSFuture} which proxies this request to another
650+
* node in the cluster.
651+
*
652+
* IMPORTANT: This is very dangerous!
653+
*
654+
* The future forwards authentication information present in the request.
655+
* Proxied requests must only be sent to trusted hosts. (We attempt to
656+
* enforce this.)
657+
*
658+
* This is not a general-purpose proxying method; it is a specialized
659+
* method with niche applications and severe security implications.
660+
*
661+
* @param string URI identifying the host we are proxying the request to.
662+
* @return HTTPSFuture New proxy future.
663+
*
664+
* @phutil-external-symbol class PhabricatorStartup
665+
*/
666+
public function newClusterProxyFuture($uri) {
667+
$uri = new PhutilURI($uri);
668+
669+
$domain = $uri->getDomain();
670+
$ip = gethostbyname($domain);
671+
if (!$ip) {
672+
throw new Exception(
673+
pht(
674+
'Unable to resolve domain "%s"!',
675+
$domain));
676+
}
677+
678+
if (!PhabricatorEnv::isClusterAddress($ip)) {
679+
throw new Exception(
680+
pht(
681+
'Refusing to proxy a request to IP address ("%s") which is not '.
682+
'in the cluster address block (this address was derived by '.
683+
'resolving the domain "%s").',
684+
$ip,
685+
$domain));
686+
}
687+
688+
$uri->setPath($this->getPath());
689+
$uri->setQueryParams(self::flattenData($_GET));
690+
691+
$input = PhabricatorStartup::getRawInput();
692+
693+
$future = id(new HTTPSFuture($uri))
694+
->addHeader('Host', self::getHost())
695+
->addHeader('X-Phabricator-Cluster', true)
696+
->setMethod($_SERVER['REQUEST_METHOD'])
697+
->write($input);
698+
699+
if (isset($_SERVER['PHP_AUTH_USER'])) {
700+
$future->setHTTPBasicAuthCredentials(
701+
$_SERVER['PHP_AUTH_USER'],
702+
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
703+
}
704+
705+
$headers = array();
706+
$seen = array();
707+
708+
// NOTE: apache_request_headers() might provide a nicer way to do this,
709+
// but isn't available under FCGI until PHP 5.4.0.
710+
foreach ($_SERVER as $key => $value) {
711+
if (preg_match('/^HTTP_/', $key)) {
712+
// Unmangle the header as best we can.
713+
$key = str_replace('_', ' ', $key);
714+
$key = strtolower($key);
715+
$key = ucwords($key);
716+
$key = str_replace(' ', '-', $key);
717+
718+
$headers[] = array($key, $value);
719+
$seen[$key] = true;
720+
}
721+
}
722+
723+
// In some situations, this may not be mapped into the HTTP_X constants.
724+
// CONTENT_LENGTH is similarly affected, but we trust cURL to take care
725+
// of that if it matters, since we're handing off a request body.
726+
if (empty($seen['Content-Type'])) {
727+
if (isset($_SERVER['CONTENT_TYPE'])) {
728+
$headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
729+
}
730+
}
731+
732+
foreach ($headers as $header) {
733+
list($key, $value) = $header;
734+
switch ($key) {
735+
case 'Host':
736+
case 'Authorization':
737+
// Don't forward these headers, we've already handled them elsewhere.
738+
unset($headers[$key]);
739+
break;
740+
default:
741+
break;
742+
}
743+
}
744+
745+
foreach ($headers as $header) {
746+
list($key, $value) = $header;
747+
$future->addHeader($key, $value);
748+
}
749+
750+
return $future;
751+
}
752+
753+
628754
}

src/applications/diffusion/controller/DiffusionServeController.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,8 @@ protected function processDiffusionRequest(AphrontRequest $request) {
205205
} else {
206206
switch ($vcs_type) {
207207
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
208-
$result = $this->serveGitRequest($repository, $viewer);
209-
break;
210208
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
211-
$result = $this->serveMercurialRequest($repository, $viewer);
209+
$result = $this->serveVCSRequest($repository, $viewer);
212210
break;
213211
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
214212
$result = new PhabricatorVCSResponse(
@@ -238,6 +236,42 @@ protected function processDiffusionRequest(AphrontRequest $request) {
238236
return $result;
239237
}
240238

239+
private function serveVCSRequest(
240+
PhabricatorRepository $repository,
241+
PhabricatorUser $viewer) {
242+
243+
// If this repository is hosted on a service, we need to proxy the request
244+
// to a host which can serve it.
245+
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
246+
247+
$uri = $repository->getAlmanacServiceURI(
248+
$viewer,
249+
$is_cluster_request,
250+
array(
251+
'http',
252+
'https',
253+
));
254+
if ($uri) {
255+
$future = $this->getRequest()->newClusterProxyFuture($uri);
256+
return id(new AphrontHTTPProxyResponse())
257+
->setHTTPFuture($future);
258+
}
259+
260+
// Otherwise, we're going to handle the request locally.
261+
262+
$vcs_type = $repository->getVersionControlSystem();
263+
switch ($vcs_type) {
264+
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
265+
$result = $this->serveGitRequest($repository, $viewer);
266+
break;
267+
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
268+
$result = $this->serveMercurialRequest($repository, $viewer);
269+
break;
270+
}
271+
272+
return $result;
273+
}
274+
241275
private function isReadOnlyRequest(
242276
PhabricatorRepository $repository) {
243277
$request = $this->getRequest();

0 commit comments

Comments
 (0)