@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+264 -8
+4
src/__phutil_library_map__.php
··· 509 509 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 510 510 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 511 511 'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php', 512 + 'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php', 512 513 'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php', 513 514 'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php', 514 515 'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php', ··· 1867 1868 'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php', 1868 1869 'PhabricatorUserTitleField' => 'applications/people/customfield/PhabricatorUserTitleField.php', 1869 1870 'PhabricatorUserTransaction' => 'applications/people/storage/PhabricatorUserTransaction.php', 1871 + 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 1870 1872 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 1871 1873 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', 1872 1874 'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php', ··· 2691 2693 'DiffusionRemarkupRule' => 'PhabricatorRemarkupRuleObject', 2692 2694 'DiffusionRepositoryController' => 'DiffusionController', 2693 2695 'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController', 2696 + 'DiffusionRepositoryDefaultController' => 'DiffusionController', 2694 2697 'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController', 2695 2698 'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController', 2696 2699 'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController', ··· 4202 4205 'PhabricatorUserTestCase' => 'PhabricatorTestCase', 4203 4206 'PhabricatorUserTitleField' => 'PhabricatorUserCustomField', 4204 4207 'PhabricatorUserTransaction' => 'PhabricatorApplicationTransaction', 4208 + 'PhabricatorVCSResponse' => 'AphrontResponse', 4205 4209 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 4206 4210 'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask', 4207 4211 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
+4
src/aphront/response/AphrontResponse.php
··· 49 49 return $this->responseCode; 50 50 } 51 51 52 + public function getHTTPResponseMessage() { 53 + return ''; 54 + } 55 + 52 56 public function setFrameable($frameable) { 53 57 $this->frameable = $frameable; 54 58 return $this;
+6 -4
src/aphront/sink/AphrontHTTPSink.php
··· 25 25 * @param int Numeric HTTP status code. 26 26 * @return void 27 27 */ 28 - final public function writeHTTPStatus($code) { 28 + final public function writeHTTPStatus($code, $message = '') { 29 29 if (!preg_match('/^\d{3}$/', $code)) { 30 30 throw new Exception("Malformed HTTP status code '{$code}'!"); 31 31 } 32 32 33 33 $code = (int)$code; 34 - $this->emitHTTPStatus($code); 34 + $this->emitHTTPStatus($code, $message); 35 35 } 36 36 37 37 ··· 103 103 $response->getHeaders(), 104 104 $response->getCacheHeaders()); 105 105 106 - $this->writeHTTPStatus($response->getHTTPResponseCode()); 106 + $this->writeHTTPStatus( 107 + $response->getHTTPResponseCode(), 108 + $response->getHTTPResponseMessage()); 107 109 $this->writeHeaders($all_headers); 108 110 $this->writeData($response_string); 109 111 } ··· 112 114 /* -( Emitting the Response )---------------------------------------------- */ 113 115 114 116 115 - abstract protected function emitHTTPStatus($code); 117 + abstract protected function emitHTTPStatus($code, $message = ''); 116 118 abstract protected function emitHeader($name, $value); 117 119 abstract protected function emitData($data); 118 120 }
+1 -1
src/aphront/sink/AphrontIsolatedHTTPSink.php
··· 11 11 private $headers; 12 12 private $data; 13 13 14 - protected function emitHTTPStatus($code) { 14 + protected function emitHTTPStatus($code, $message = '') { 15 15 $this->status = $code; 16 16 } 17 17
+6 -2
src/aphront/sink/AphrontPHPHTTPSink.php
··· 7 7 */ 8 8 final class AphrontPHPHTTPSink extends AphrontHTTPSink { 9 9 10 - protected function emitHTTPStatus($code) { 10 + protected function emitHTTPStatus($code, $message = '') { 11 11 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); 13 17 } 14 18 } 15 19
+1 -1
src/applications/base/controller/PhabricatorController.php
··· 24 24 return PhabricatorUserEmail::isEmailVerificationRequired(); 25 25 } 26 26 27 - final public function willBeginExecution() { 27 + public function willBeginExecution() { 28 28 29 29 $request = $this->getRequest(); 30 30 if ($request->getUser()) {
+7
src/applications/diffusion/application/PhabricatorApplicationDiffusion.php
··· 79 79 '(?P<serve>serve)/' => 'DiffusionRepositoryEditHostingController', 80 80 ), 81 81 ), 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 + 82 89 'inline/' => array( 83 90 'edit/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController', 84 91 'preview/(?P<phid>[^/]+)/' =>
+162
src/applications/diffusion/controller/DiffusionController.php
··· 4 4 5 5 protected $diffusionRequest; 6 6 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 + 7 169 public function willProcessRequest(array $data) { 8 170 if (isset($data['callsign'])) { 9 171 $drequest = DiffusionRequest::newFromAphrontRequestDictionary(
+11
src/applications/diffusion/controller/DiffusionRepositoryDefaultController.php
··· 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 + }
+62
src/applications/repository/response/PhabricatorVCSResponse.php
··· 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 + }