Skip to content

Commit c7f23f5

Browse files
author
epriestley
committed
Accept and route VCS HTTP requests
Summary: Mostly ripped from D7391, with some changes: - Serve repositories at `/diffusion/X/`, with no special `/git/` or `/serve/` URI component. - This requires a little bit of magic, but I got the magic working for Git, Mercurial and SVN, and it seems reasonable. - I think having one URI for everything will make it easier for users to understand. - One downside is that git will clone into `X` by default, but I think that's not a big deal, and we can work around that in the future easily enough. - Accept HTTP requests for Git, SVN and Mercurial repositories. - Auth logic is a little different in order to be more consistent with how other things work. - Instead of AphrontBasicAuthResponse, added "VCSResponse". Mercurial can print strings we send it on the CLI if we're careful, so support that. I did a fair amount of digging and didn't have any luck with git or svn. - Commands we don't know about are assumed to require "Push" capability by default. No actual VCS data going over the wire yet. Test Plan: Ran a bunch of stuff like this: $ hg clone http://local.aphront.com:8080/diffusion/P/ abort: HTTP Error 403: This repository is not available over HTTP. ...and got pretty reasonable-seeming errors in all cases. All this can do is produce errors for now. Reviewers: hach-que, btrahan Reviewed By: hach-que CC: aran Maniphest Tasks: T2230 Differential Revision: https://secure.phabricator.com/D7417
1 parent bb35f8e commit c7f23f5

File tree

10 files changed

+264
-8
lines changed

10 files changed

+264
-8
lines changed

src/__phutil_library_map__.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@
509509
'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php',
510510
'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php',
511511
'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php',
512+
'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php',
512513
'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php',
513514
'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php',
514515
'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php',
@@ -1867,6 +1868,7 @@
18671868
'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
18681869
'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php',
18691870
'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php',
1871+
'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php',
18701872
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
18711873
'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
18721874
'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
@@ -2691,6 +2693,7 @@
26912693
'DiffusionRemarkupRule' => 'PhabricatorRemarkupRuleObject',
26922694
'DiffusionRepositoryController' => 'DiffusionController',
26932695
'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController',
2696+
'DiffusionRepositoryDefaultController' => 'DiffusionController',
26942697
'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController',
26952698
'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController',
26962699
'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController',
@@ -4202,6 +4205,7 @@
42024205
'PhabricatorUserTestCase' => 'PhabricatorTestCase',
42034206
'PhabricatorUserTitleField' => 'PhabricatorUserCustomField',
42044207
'PhabricatorUserTransaction' => 'PhabricatorApplicationTransaction',
4208+
'PhabricatorVCSResponse' => 'AphrontResponse',
42054209
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
42064210
'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
42074211
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',

src/aphront/response/AphrontResponse.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public function getHTTPResponseCode() {
4949
return $this->responseCode;
5050
}
5151

52+
public function getHTTPResponseMessage() {
53+
return '';
54+
}
55+
5256
public function setFrameable($frameable) {
5357
$this->frameable = $frameable;
5458
return $this;

src/aphront/sink/AphrontHTTPSink.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ abstract class AphrontHTTPSink {
2525
* @param int Numeric HTTP status code.
2626
* @return void
2727
*/
28-
final public function writeHTTPStatus($code) {
28+
final public function writeHTTPStatus($code, $message = '') {
2929
if (!preg_match('/^\d{3}$/', $code)) {
3030
throw new Exception("Malformed HTTP status code '{$code}'!");
3131
}
3232

3333
$code = (int)$code;
34-
$this->emitHTTPStatus($code);
34+
$this->emitHTTPStatus($code, $message);
3535
}
3636

3737

@@ -103,7 +103,9 @@ final public function writeResponse(AphrontResponse $response) {
103103
$response->getHeaders(),
104104
$response->getCacheHeaders());
105105

106-
$this->writeHTTPStatus($response->getHTTPResponseCode());
106+
$this->writeHTTPStatus(
107+
$response->getHTTPResponseCode(),
108+
$response->getHTTPResponseMessage());
107109
$this->writeHeaders($all_headers);
108110
$this->writeData($response_string);
109111
}
@@ -112,7 +114,7 @@ final public function writeResponse(AphrontResponse $response) {
112114
/* -( Emitting the Response )---------------------------------------------- */
113115

114116

115-
abstract protected function emitHTTPStatus($code);
117+
abstract protected function emitHTTPStatus($code, $message = '');
116118
abstract protected function emitHeader($name, $value);
117119
abstract protected function emitData($data);
118120
}

src/aphront/sink/AphrontIsolatedHTTPSink.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ final class AphrontIsolatedHTTPSink extends AphrontHTTPSink {
1111
private $headers;
1212
private $data;
1313

14-
protected function emitHTTPStatus($code) {
14+
protected function emitHTTPStatus($code, $message = '') {
1515
$this->status = $code;
1616
}
1717

src/aphront/sink/AphrontPHPHTTPSink.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
*/
88
final class AphrontPHPHTTPSink extends AphrontHTTPSink {
99

10-
protected function emitHTTPStatus($code) {
10+
protected function emitHTTPStatus($code, $message = '') {
1111
if ($code != 200) {
12-
header("HTTP/1.0 {$code}");
12+
$header = "HTTP/1.0 {$code}";
13+
if (strlen($message)) {
14+
$header .= " {$message}";
15+
}
16+
header($header);
1317
}
1418
}
1519

src/applications/base/controller/PhabricatorController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function shouldRequireEmailVerification() {
2424
return PhabricatorUserEmail::isEmailVerificationRequired();
2525
}
2626

27-
final public function willBeginExecution() {
27+
public function willBeginExecution() {
2828

2929
$request = $this->getRequest();
3030
if ($request->getUser()) {

src/applications/diffusion/application/PhabricatorApplicationDiffusion.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ public function getRoutes() {
7979
'(?P<serve>serve)/' => 'DiffusionRepositoryEditHostingController',
8080
),
8181
),
82+
83+
// NOTE: This must come after the rule above; it just gives us a
84+
// catch-all for serving repositories over HTTP. We must accept
85+
// requests without the trailing "/" because SVN commands don't
86+
// necessarily include it.
87+
'(?P<callsign>[A-Z]+)(/|$).*' => 'DiffusionRepositoryDefaultController',
88+
8289
'inline/' => array(
8390
'edit/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController',
8491
'preview/(?P<phid>[^/]+)/' =>

src/applications/diffusion/controller/DiffusionController.php

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,168 @@ abstract class DiffusionController extends PhabricatorController {
44

55
protected $diffusionRequest;
66

7+
public function willBeginExecution() {
8+
$request = $this->getRequest();
9+
$uri = $request->getRequestURI();
10+
11+
// Check if this is a VCS request, e.g. from "git clone", "hg clone", or
12+
// "svn checkout". If it is, we jump off into repository serving code to
13+
// process the request.
14+
15+
$regex = '@^/diffusion/(?P<callsign>[A-Z]+)(/|$)@';
16+
$matches = null;
17+
if (preg_match($regex, (string)$uri, $matches)) {
18+
$vcs = null;
19+
20+
if ($request->getExists('__vcs__')) {
21+
// This is magic to make it easier for us to debug stuff by telling
22+
// users to run:
23+
//
24+
// curl http://example.phabricator.com/diffusion/X/?__vcs__=1
25+
//
26+
// ...to get a human-readable error.
27+
$vcs = $request->getExists('__vcs__');
28+
} else if ($request->getExists('service')) {
29+
// Git also gives us a User-Agent like "git/1.8.2.3".
30+
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
31+
} else if ($request->getExists('cmd')) {
32+
// Mercurial also sends an Accept header like
33+
// "application/mercurial-0.1", and a User-Agent like
34+
// "mercurial/proto-1.0".
35+
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
36+
} else {
37+
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
38+
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
39+
// serf/1.3.2".
40+
$dav = $request->getHTTPHeader('DAV');
41+
$dav = new PhutilURI($dav);
42+
if ($dav->getDomain() === 'subversion.tigris.org') {
43+
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
44+
}
45+
}
46+
47+
if ($vcs) {
48+
return $this->processVCSRequest($matches['callsign']);
49+
}
50+
}
51+
52+
parent::willBeginExecution();
53+
}
54+
55+
private function processVCSRequest($callsign) {
56+
57+
// TODO: Authenticate user.
58+
59+
$viewer = new PhabricatorUser();
60+
61+
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
62+
if (!$allow_public) {
63+
if (!$viewer->isLoggedIn()) {
64+
return new PhabricatorVCSResponse(
65+
403,
66+
pht('You must log in to access repositories.'));
67+
}
68+
}
69+
70+
try {
71+
$repository = id(new PhabricatorRepositoryQuery())
72+
->setViewer($viewer)
73+
->withCallsigns(array($callsign))
74+
->executeOne();
75+
if (!$repository) {
76+
return new PhabricatorVCSResponse(
77+
404,
78+
pht('No such repository exists.'));
79+
}
80+
} catch (PhabricatorPolicyException $ex) {
81+
if ($viewer->isLoggedIn()) {
82+
return new PhabricatorVCSResponse(
83+
403,
84+
pht('You do not have permission to access this repository.'));
85+
} else {
86+
return new PhabricatorVCSResponse(
87+
401,
88+
pht('You must log in to access this repository.'));
89+
}
90+
}
91+
92+
$is_push = !$this->isReadOnlyRequest($repository);
93+
94+
switch ($repository->getServeOverHTTP()) {
95+
case PhabricatorRepository::SERVE_READONLY:
96+
if ($is_push) {
97+
return new PhabricatorVCSResponse(
98+
403,
99+
pht('This repository is read-only over HTTP.'));
100+
}
101+
break;
102+
case PhabricatorRepository::SERVE_READWRITE:
103+
if ($is_push) {
104+
$can_push = PhabricatorPolicyFilter::hasCapability(
105+
$viewer,
106+
$repository,
107+
DiffusionCapabilityPush::CAPABILITY);
108+
if (!$can_push) {
109+
if ($viewer->isLoggedIn()) {
110+
return new PhabricatorVCSResponse(
111+
403,
112+
pht('You do not have permission to push to this repository.'));
113+
} else {
114+
return new PhabricatorVCSResponse(
115+
401,
116+
pht('You must log in to push to this repository.'));
117+
}
118+
}
119+
}
120+
break;
121+
case PhabricatorRepository::SERVE_OFF:
122+
default:
123+
return new PhabricatorVCSResponse(
124+
403,
125+
pht('This repository is not available over HTTP.'));
126+
}
127+
128+
return new PhabricatorVCSResponse(
129+
999,
130+
pht('TODO: Implement meaningful responses.'));
131+
}
132+
133+
private function isReadOnlyRequest(
134+
PhabricatorRepository $repository) {
135+
$request = $this->getRequest();
136+
137+
// TODO: This implementation is safe by default, but very incomplete.
138+
139+
switch ($repository->getVersionControlSystem()) {
140+
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
141+
$service = $request->getStr('service');
142+
// NOTE: Service names are the reverse of what you might expect, as they
143+
// are from the point of view of the server. The main read service is
144+
// "git-upload-pack", and the main write service is "git-receive-pack".
145+
switch ($service) {
146+
case 'git-upload-pack':
147+
return true;
148+
case 'git-receive-pack':
149+
default:
150+
return false;
151+
}
152+
break;
153+
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
154+
$cmd = $request->getStr('cmd');
155+
switch ($cmd) {
156+
case 'capabilities':
157+
return true;
158+
default:
159+
return false;
160+
}
161+
break;
162+
case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
163+
break;
164+
}
165+
166+
return false;
167+
}
168+
7169
public function willProcessRequest(array $data) {
8170
if (isset($data['callsign'])) {
9171
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
final class DiffusionRepositoryDefaultController extends DiffusionController {
4+
5+
public function processRequest() {
6+
// NOTE: This controller is just here to make sure we call
7+
// willBeginExecution() on any /diffusion/X/ URI, so we can intercept
8+
// `git`, `hg` and `svn` HTTP protocol requests.
9+
return new Aphront404Response();
10+
}
11+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/**
4+
* In Git, there appears to be no way to send a message which will be output
5+
* by `git clone http://...`, although the response code is visible.
6+
*
7+
* In Mercurial, the HTTP status response message is printed to the console, so
8+
* we send human-readable text there.
9+
*
10+
* In Subversion, we can get it to print a custom message if we send an
11+
* invalid/unknown response code, although the output is ugly and difficult
12+
* to read. For known codes like 404, it prints a canned message.
13+
*
14+
* All VCS binaries ignore the response body; we include it only for
15+
* completeness.
16+
*/
17+
final class PhabricatorVCSResponse extends AphrontResponse {
18+
19+
private $code;
20+
private $message;
21+
22+
public function __construct($code, $message) {
23+
$this->code = $code;
24+
25+
$message = head(phutil_split_lines($message));
26+
$this->message = $message;
27+
}
28+
29+
public function getMessage() {
30+
return $this->message;
31+
}
32+
33+
public function buildResponseString() {
34+
return $this->code.' '.$this->message;
35+
}
36+
37+
public function getHeaders() {
38+
$headers = array();
39+
40+
if ($this->getHTTPResponseCode() == 401) {
41+
$headers[] = array(
42+
'WWW-Authenticate',
43+
'Basic realm="Phabricator Repositories"',
44+
);
45+
}
46+
47+
return $headers;
48+
}
49+
50+
public function getCacheHeaders() {
51+
return array();
52+
}
53+
54+
public function getHTTPResponseCode() {
55+
return $this->code;
56+
}
57+
58+
public function getHTTPResponseMessage() {
59+
return $this->message;
60+
}
61+
62+
}

0 commit comments

Comments
 (0)