@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.

Split Diffusion VSC serve code into its own controller

Summary: This is starting to get a bit sizable and it turns out Mercurial is sort of a beast, so split the VCS serve stuff into a separate controller.

Test Plan: Pushed and pulled an authenticated Git repository.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran, hach-que

Differential Revision: https://secure.phabricator.com/D7494

+368 -354
+2
src/__phutil_library_map__.php
··· 541 541 'DiffusionSSHGitUploadPackWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php', 542 542 'DiffusionSSHGitWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitWorkflow.php', 543 543 'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', 544 + 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', 544 545 'DiffusionSetPasswordPanel' => 'applications/diffusion/panel/DiffusionSetPasswordPanel.php', 545 546 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 546 547 'DiffusionSvnCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php', ··· 2787 2788 'DiffusionSSHGitUploadPackWorkflow' => 'DiffusionSSHGitWorkflow', 2788 2789 'DiffusionSSHGitWorkflow' => 'DiffusionSSHWorkflow', 2789 2790 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 2791 + 'DiffusionServeController' => 'DiffusionController', 2790 2792 'DiffusionSetPasswordPanel' => 'PhabricatorSettingsPanel', 2791 2793 'DiffusionSetupException' => 'AphrontUsageException', 2792 2794 'DiffusionSvnCommitParentsQuery' => 'DiffusionCommitParentsQuery',
+5 -354
src/applications/diffusion/controller/DiffusionController.php
··· 6 6 7 7 public function willBeginExecution() { 8 8 $request = $this->getRequest(); 9 - $uri = $request->getRequestURI(); 10 - 11 - $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); 12 9 13 10 // Check if this is a VCS request, e.g. from "git clone", "hg clone", or 14 11 // "svn checkout". If it is, we jump off into repository serving code to 15 12 // process the request. 16 - 17 - $regex = '@^/diffusion/(?P<callsign>[A-Z]+)(/|$)@'; 18 - $matches = null; 19 - if (preg_match($regex, (string)$uri, $matches)) { 20 - $vcs = null; 21 - 22 - $content_type = $request->getHTTPHeader('Content-Type'); 23 - 24 - if ($request->getExists('__vcs__')) { 25 - // This is magic to make it easier for us to debug stuff by telling 26 - // users to run: 27 - // 28 - // curl http://example.phabricator.com/diffusion/X/?__vcs__=1 29 - // 30 - // ...to get a human-readable error. 31 - $vcs = $request->getExists('__vcs__'); 32 - } else if (strncmp($user_agent, "git/", 4) === 0) { 33 - $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 34 - } else if ($request->getExists('service')) { 35 - $service = $request->getStr('service'); 36 - // We get this initially for `info/refs`. 37 - // Git also gives us a User-Agent like "git/1.8.2.3". 38 - $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 39 - } else if ($content_type == 'application/x-git-upload-pack-request') { 40 - // We get this for `git-upload-pack`. 41 - $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 42 - } else if ($content_type == 'application/x-git-receive-pack-request') { 43 - // We get this for `git-receive-pack`. 44 - $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 45 - } else if ($request->getExists('cmd')) { 46 - // Mercurial also sends an Accept header like 47 - // "application/mercurial-0.1", and a User-Agent like 48 - // "mercurial/proto-1.0". 49 - $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; 50 - } else { 51 - // Subversion also sends an initial OPTIONS request (vs GET/POST), and 52 - // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2) 53 - // serf/1.3.2". 54 - $dav = $request->getHTTPHeader('DAV'); 55 - $dav = new PhutilURI($dav); 56 - if ($dav->getDomain() === 'subversion.tigris.org') { 57 - $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; 58 - } 59 - } 60 - 61 - if ($vcs) { 62 - return $this->processVCSRequest($matches['callsign']); 63 - } 13 + if (DiffusionServeController::isVCSRequest($request)) { 14 + $serve_controller = id(new DiffusionServeController($request)) 15 + ->setCurrentApplication($this->getCurrentApplication()); 16 + return $this->delegateToController($serve_controller); 64 17 } 65 18 66 - parent::willBeginExecution(); 67 - } 68 - 69 - private function processVCSRequest($callsign) { 70 - 71 - // If authentication credentials have been provided, try to find a user 72 - // that actually matches those credentials. 73 - if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { 74 - $username = $_SERVER['PHP_AUTH_USER']; 75 - $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); 76 - 77 - $viewer = $this->authenticateHTTPRepositoryUser($username, $password); 78 - if (!$viewer) { 79 - return new PhabricatorVCSResponse( 80 - 403, 81 - pht('Invalid credentials.')); 82 - } 83 - } else { 84 - // User hasn't provided credentials, which means we count them as 85 - // being "not logged in". 86 - $viewer = new PhabricatorUser(); 87 - } 88 - 89 - $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); 90 - $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); 91 - if (!$allow_public) { 92 - if (!$viewer->isLoggedIn()) { 93 - if ($allow_auth) { 94 - return new PhabricatorVCSResponse( 95 - 401, 96 - pht('You must log in to access repositories.')); 97 - } else { 98 - return new PhabricatorVCSResponse( 99 - 403, 100 - pht('Public and authenticated HTTP access are both forbidden.')); 101 - } 102 - } 103 - } 104 - 105 - try { 106 - $repository = id(new PhabricatorRepositoryQuery()) 107 - ->setViewer($viewer) 108 - ->withCallsigns(array($callsign)) 109 - ->executeOne(); 110 - if (!$repository) { 111 - return new PhabricatorVCSResponse( 112 - 404, 113 - pht('No such repository exists.')); 114 - } 115 - } catch (PhabricatorPolicyException $ex) { 116 - if ($viewer->isLoggedIn()) { 117 - return new PhabricatorVCSResponse( 118 - 403, 119 - pht('You do not have permission to access this repository.')); 120 - } else { 121 - if ($allow_auth) { 122 - return new PhabricatorVCSResponse( 123 - 401, 124 - pht('You must log in to access this repository.')); 125 - } else { 126 - return new PhabricatorVCSResponse( 127 - 403, 128 - pht( 129 - 'This repository requires authentication, which is forbidden '. 130 - 'over HTTP.')); 131 - } 132 - } 133 - } 134 - 135 - if (!$repository->isTracked()) { 136 - return new PhabricatorVCSResponse( 137 - 403, 138 - pht('This repository is inactive.')); 139 - } 140 - 141 - $is_push = !$this->isReadOnlyRequest($repository); 142 - 143 - switch ($repository->getServeOverHTTP()) { 144 - case PhabricatorRepository::SERVE_READONLY: 145 - if ($is_push) { 146 - return new PhabricatorVCSResponse( 147 - 403, 148 - pht('This repository is read-only over HTTP.')); 149 - } 150 - break; 151 - case PhabricatorRepository::SERVE_READWRITE: 152 - if ($is_push) { 153 - $can_push = PhabricatorPolicyFilter::hasCapability( 154 - $viewer, 155 - $repository, 156 - DiffusionCapabilityPush::CAPABILITY); 157 - if (!$can_push) { 158 - if ($viewer->isLoggedIn()) { 159 - return new PhabricatorVCSResponse( 160 - 403, 161 - pht('You do not have permission to push to this repository.')); 162 - } else { 163 - if ($allow_auth) { 164 - return new PhabricatorVCSResponse( 165 - 401, 166 - pht('You must log in to push to this repository.')); 167 - } else { 168 - return new PhabricatorVCSResponse( 169 - 403, 170 - pht( 171 - 'Pushing to this repository requires authentication, '. 172 - 'which is forbidden over HTTP.')); 173 - } 174 - } 175 - } 176 - } 177 - break; 178 - case PhabricatorRepository::SERVE_OFF: 179 - default: 180 - return new PhabricatorVCSResponse( 181 - 403, 182 - pht('This repository is not available over HTTP.')); 183 - } 184 - 185 - switch ($repository->getVersionControlSystem()) { 186 - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 187 - $result = $this->serveGitRequest($repository, $viewer); 188 - break; 189 - default: 190 - $result = new PhabricatorVCSResponse( 191 - 999, 192 - pht('TODO: Implement meaningful responses.')); 193 - break; 194 - } 195 - 196 - $code = $result->getHTTPResponseCode(); 197 - 198 - if ($is_push && ($code == 200)) { 199 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 200 - $repository->writeStatusMessage( 201 - PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, 202 - PhabricatorRepositoryStatusMessage::CODE_OKAY); 203 - unset($unguarded); 204 - } 205 - 206 - return $result; 207 - } 208 - 209 - private function isReadOnlyRequest( 210 - PhabricatorRepository $repository) { 211 - $request = $this->getRequest(); 212 - $method = $_SERVER['REQUEST_METHOD']; 213 - 214 - // TODO: This implementation is safe by default, but very incomplete. 215 - 216 - switch ($repository->getVersionControlSystem()) { 217 - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 218 - $service = $request->getStr('service'); 219 - $path = $this->getRequestDirectoryPath(); 220 - // NOTE: Service names are the reverse of what you might expect, as they 221 - // are from the point of view of the server. The main read service is 222 - // "git-upload-pack", and the main write service is "git-receive-pack". 223 - 224 - if ($method == 'GET' && 225 - $path == '/info/refs' && 226 - $service == 'git-upload-pack') { 227 - return true; 228 - } 229 - 230 - if ($path == '/git-upload-pack') { 231 - return true; 232 - } 233 - 234 - break; 235 - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 236 - $cmd = $request->getStr('cmd'); 237 - switch ($cmd) { 238 - case 'capabilities': 239 - return true; 240 - default: 241 - return false; 242 - } 243 - break; 244 - case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION: 245 - break; 246 - } 247 - 248 - return false; 19 + return parent::willBeginExecution(); 249 20 } 250 21 251 22 public function willProcessRequest(array $data) { ··· 457 228 return $links; 458 229 } 459 230 460 - /** 461 - * @phutil-external-symbol class PhabricatorStartup 462 - */ 463 - private function serveGitRequest( 464 - PhabricatorRepository $repository, 465 - PhabricatorUser $viewer) { 466 - $request = $this->getRequest(); 467 - 468 - $request_path = $this->getRequestDirectoryPath(); 469 - $repository_root = $repository->getLocalPath(); 470 - 471 - // Rebuild the query string to strip `__magic__` parameters and prevent 472 - // issues where we might interpret inputs like "service=read&service=write" 473 - // differently than the server does and pass it an unsafe command. 474 - 475 - // NOTE: This does not use getPassthroughRequestParameters() because 476 - // that code is HTTP-method agnostic and will encode POST data. 477 - 478 - $query_data = $_GET; 479 - foreach ($query_data as $key => $value) { 480 - if (!strncmp($key, '__', 2)) { 481 - unset($query_data[$key]); 482 - } 483 - } 484 - $query_string = http_build_query($query_data, '', '&'); 485 - 486 - // We're about to wipe out PATH with the rest of the environment, so 487 - // resolve the binary first. 488 - $bin = Filesystem::resolveBinary('git-http-backend'); 489 - if (!$bin) { 490 - throw new Exception("Unable to find `git-http-backend` in PATH!"); 491 - } 492 - 493 - $env = array( 494 - 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 495 - 'QUERY_STRING' => $query_string, 496 - 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), 497 - 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 498 - 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 499 - 'GIT_PROJECT_ROOT' => $repository_root, 500 - 'GIT_HTTP_EXPORT_ALL' => '1', 501 - 'PATH_INFO' => $request_path, 502 - 503 - 'REMOTE_USER' => $viewer->getUsername(), 504 - 505 - // TODO: Set these correctly. 506 - // GIT_COMMITTER_NAME 507 - // GIT_COMMITTER_EMAIL 508 - ); 509 - 510 - $input = PhabricatorStartup::getRawInput(); 511 - 512 - list($err, $stdout, $stderr) = id(new ExecFuture('%s', $bin)) 513 - ->setEnv($env, true) 514 - ->write($input) 515 - ->resolve(); 516 - 517 - if ($err) { 518 - return new PhabricatorVCSResponse( 519 - 500, 520 - pht('Error %d: %s', $err, $stderr)); 521 - } 522 - 523 - return id(new DiffusionGitResponse())->setGitData($stdout); 524 - } 525 - 526 - private function getRequestDirectoryPath() { 527 - $request = $this->getRequest(); 528 - $request_path = $request->getRequestURI()->getPath(); 529 - return preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); 530 - } 531 - 532 231 protected function renderStatusMessage($title, $body) { 533 232 return id(new AphrontErrorView()) 534 233 ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ··· 536 235 ->appendChild($body); 537 236 } 538 237 539 - private function authenticateHTTPRepositoryUser( 540 - $username, 541 - PhutilOpaqueEnvelope $password) { 542 - 543 - if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { 544 - // No HTTP auth permitted. 545 - return null; 546 - } 547 - 548 - if (!strlen($username)) { 549 - // No username. 550 - return null; 551 - } 552 - 553 - if (!strlen($password->openEnvelope())) { 554 - // No password. 555 - return null; 556 - } 557 - 558 - $user = id(new PhabricatorPeopleQuery()) 559 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 560 - ->withUsernames(array($username)) 561 - ->executeOne(); 562 - if (!$user) { 563 - // Username doesn't match anything. 564 - return null; 565 - } 566 - 567 - $password_entry = id(new PhabricatorRepositoryVCSPassword()) 568 - ->loadOneWhere('userPHID = %s', $user->getPHID()); 569 - if (!$password_entry) { 570 - // User doesn't have a password set. 571 - return null; 572 - } 573 - 574 - if (!$password_entry->comparePassword($password, $user)) { 575 - // Password doesn't match. 576 - return null; 577 - } 578 - 579 - if ($user->getIsDisabled()) { 580 - // User is disabled. 581 - return null; 582 - } 583 - 584 - return $user; 585 - } 586 238 } 587 -
+361
src/applications/diffusion/controller/DiffusionServeController.php
··· 1 + <?php 2 + 3 + final class DiffusionServeController extends DiffusionController { 4 + 5 + public static function isVCSRequest(AphrontRequest $request) { 6 + if (!self::getCallsign($request)) { 7 + return null; 8 + } 9 + 10 + $content_type = $request->getHTTPHeader('Content-Type'); 11 + $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); 12 + 13 + $vcs = null; 14 + if ($request->getExists('service')) { 15 + $service = $request->getStr('service'); 16 + // We get this initially for `info/refs`. 17 + // Git also gives us a User-Agent like "git/1.8.2.3". 18 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 19 + } else if (strncmp($user_agent, "git/", 4) === 0) { 20 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 21 + } else if ($content_type == 'application/x-git-upload-pack-request') { 22 + // We get this for `git-upload-pack`. 23 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 24 + } else if ($content_type == 'application/x-git-receive-pack-request') { 25 + // We get this for `git-receive-pack`. 26 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 27 + } else if ($request->getExists('cmd')) { 28 + // Mercurial also sends an Accept header like 29 + // "application/mercurial-0.1", and a User-Agent like 30 + // "mercurial/proto-1.0". 31 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; 32 + } else { 33 + // Subversion also sends an initial OPTIONS request (vs GET/POST), and 34 + // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2) 35 + // serf/1.3.2". 36 + $dav = $request->getHTTPHeader('DAV'); 37 + $dav = new PhutilURI($dav); 38 + if ($dav->getDomain() === 'subversion.tigris.org') { 39 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; 40 + } 41 + } 42 + 43 + return $vcs; 44 + } 45 + 46 + private static function getCallsign(AphrontRequest $request) { 47 + $uri = $request->getRequestURI(); 48 + 49 + $regex = '@^/diffusion/(?P<callsign>[A-Z]+)(/|$)@'; 50 + $matches = null; 51 + if (!preg_match($regex, (string)$uri, $matches)) { 52 + return null; 53 + } 54 + return $matches['callsign']; 55 + } 56 + 57 + public function processRequest() { 58 + $request = $this->getRequest(); 59 + $callsign = self::getCallsign($request); 60 + 61 + // If authentication credentials have been provided, try to find a user 62 + // that actually matches those credentials. 63 + if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { 64 + $username = $_SERVER['PHP_AUTH_USER']; 65 + $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); 66 + 67 + $viewer = $this->authenticateHTTPRepositoryUser($username, $password); 68 + if (!$viewer) { 69 + return new PhabricatorVCSResponse( 70 + 403, 71 + pht('Invalid credentials.')); 72 + } 73 + } else { 74 + // User hasn't provided credentials, which means we count them as 75 + // being "not logged in". 76 + $viewer = new PhabricatorUser(); 77 + } 78 + 79 + $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); 80 + $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); 81 + if (!$allow_public) { 82 + if (!$viewer->isLoggedIn()) { 83 + if ($allow_auth) { 84 + return new PhabricatorVCSResponse( 85 + 401, 86 + pht('You must log in to access repositories.')); 87 + } else { 88 + return new PhabricatorVCSResponse( 89 + 403, 90 + pht('Public and authenticated HTTP access are both forbidden.')); 91 + } 92 + } 93 + } 94 + 95 + try { 96 + $repository = id(new PhabricatorRepositoryQuery()) 97 + ->setViewer($viewer) 98 + ->withCallsigns(array($callsign)) 99 + ->executeOne(); 100 + if (!$repository) { 101 + return new PhabricatorVCSResponse( 102 + 404, 103 + pht('No such repository exists.')); 104 + } 105 + } catch (PhabricatorPolicyException $ex) { 106 + if ($viewer->isLoggedIn()) { 107 + return new PhabricatorVCSResponse( 108 + 403, 109 + pht('You do not have permission to access this repository.')); 110 + } else { 111 + if ($allow_auth) { 112 + return new PhabricatorVCSResponse( 113 + 401, 114 + pht('You must log in to access this repository.')); 115 + } else { 116 + return new PhabricatorVCSResponse( 117 + 403, 118 + pht( 119 + 'This repository requires authentication, which is forbidden '. 120 + 'over HTTP.')); 121 + } 122 + } 123 + } 124 + 125 + if (!$repository->isTracked()) { 126 + return new PhabricatorVCSResponse( 127 + 403, 128 + pht('This repository is inactive.')); 129 + } 130 + 131 + $is_push = !$this->isReadOnlyRequest($repository); 132 + 133 + switch ($repository->getServeOverHTTP()) { 134 + case PhabricatorRepository::SERVE_READONLY: 135 + if ($is_push) { 136 + return new PhabricatorVCSResponse( 137 + 403, 138 + pht('This repository is read-only over HTTP.')); 139 + } 140 + break; 141 + case PhabricatorRepository::SERVE_READWRITE: 142 + if ($is_push) { 143 + $can_push = PhabricatorPolicyFilter::hasCapability( 144 + $viewer, 145 + $repository, 146 + DiffusionCapabilityPush::CAPABILITY); 147 + if (!$can_push) { 148 + if ($viewer->isLoggedIn()) { 149 + return new PhabricatorVCSResponse( 150 + 403, 151 + pht('You do not have permission to push to this repository.')); 152 + } else { 153 + if ($allow_auth) { 154 + return new PhabricatorVCSResponse( 155 + 401, 156 + pht('You must log in to push to this repository.')); 157 + } else { 158 + return new PhabricatorVCSResponse( 159 + 403, 160 + pht( 161 + 'Pushing to this repository requires authentication, '. 162 + 'which is forbidden over HTTP.')); 163 + } 164 + } 165 + } 166 + } 167 + break; 168 + case PhabricatorRepository::SERVE_OFF: 169 + default: 170 + return new PhabricatorVCSResponse( 171 + 403, 172 + pht('This repository is not available over HTTP.')); 173 + } 174 + 175 + switch ($repository->getVersionControlSystem()) { 176 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 177 + $result = $this->serveGitRequest($repository, $viewer); 178 + break; 179 + default: 180 + $result = new PhabricatorVCSResponse( 181 + 999, 182 + pht('TODO: Implement meaningful responses.')); 183 + break; 184 + } 185 + 186 + $code = $result->getHTTPResponseCode(); 187 + 188 + if ($is_push && ($code == 200)) { 189 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 190 + $repository->writeStatusMessage( 191 + PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, 192 + PhabricatorRepositoryStatusMessage::CODE_OKAY); 193 + unset($unguarded); 194 + } 195 + 196 + return $result; 197 + } 198 + 199 + private function isReadOnlyRequest( 200 + PhabricatorRepository $repository) { 201 + $request = $this->getRequest(); 202 + $method = $_SERVER['REQUEST_METHOD']; 203 + 204 + // TODO: This implementation is safe by default, but very incomplete. 205 + 206 + switch ($repository->getVersionControlSystem()) { 207 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 208 + $service = $request->getStr('service'); 209 + $path = $this->getRequestDirectoryPath(); 210 + // NOTE: Service names are the reverse of what you might expect, as they 211 + // are from the point of view of the server. The main read service is 212 + // "git-upload-pack", and the main write service is "git-receive-pack". 213 + 214 + if ($method == 'GET' && 215 + $path == '/info/refs' && 216 + $service == 'git-upload-pack') { 217 + return true; 218 + } 219 + 220 + if ($path == '/git-upload-pack') { 221 + return true; 222 + } 223 + 224 + break; 225 + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 226 + $cmd = $request->getStr('cmd'); 227 + switch ($cmd) { 228 + case 'capabilities': 229 + return true; 230 + default: 231 + return false; 232 + } 233 + break; 234 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION: 235 + break; 236 + } 237 + 238 + return false; 239 + } 240 + 241 + /** 242 + * @phutil-external-symbol class PhabricatorStartup 243 + */ 244 + private function serveGitRequest( 245 + PhabricatorRepository $repository, 246 + PhabricatorUser $viewer) { 247 + $request = $this->getRequest(); 248 + 249 + $request_path = $this->getRequestDirectoryPath(); 250 + $repository_root = $repository->getLocalPath(); 251 + 252 + // Rebuild the query string to strip `__magic__` parameters and prevent 253 + // issues where we might interpret inputs like "service=read&service=write" 254 + // differently than the server does and pass it an unsafe command. 255 + 256 + // NOTE: This does not use getPassthroughRequestParameters() because 257 + // that code is HTTP-method agnostic and will encode POST data. 258 + 259 + $query_data = $_GET; 260 + foreach ($query_data as $key => $value) { 261 + if (!strncmp($key, '__', 2)) { 262 + unset($query_data[$key]); 263 + } 264 + } 265 + $query_string = http_build_query($query_data, '', '&'); 266 + 267 + // We're about to wipe out PATH with the rest of the environment, so 268 + // resolve the binary first. 269 + $bin = Filesystem::resolveBinary('git-http-backend'); 270 + if (!$bin) { 271 + throw new Exception("Unable to find `git-http-backend` in PATH!"); 272 + } 273 + 274 + $env = array( 275 + 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 276 + 'QUERY_STRING' => $query_string, 277 + 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), 278 + 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 279 + 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 280 + 'GIT_PROJECT_ROOT' => $repository_root, 281 + 'GIT_HTTP_EXPORT_ALL' => '1', 282 + 'PATH_INFO' => $request_path, 283 + 284 + 'REMOTE_USER' => $viewer->getUsername(), 285 + 286 + // TODO: Set these correctly. 287 + // GIT_COMMITTER_NAME 288 + // GIT_COMMITTER_EMAIL 289 + ); 290 + 291 + $input = PhabricatorStartup::getRawInput(); 292 + 293 + list($err, $stdout, $stderr) = id(new ExecFuture('%s', $bin)) 294 + ->setEnv($env, true) 295 + ->write($input) 296 + ->resolve(); 297 + 298 + if ($err) { 299 + return new PhabricatorVCSResponse( 300 + 500, 301 + pht('Error %d: %s', $err, $stderr)); 302 + } 303 + 304 + return id(new DiffusionGitResponse())->setGitData($stdout); 305 + } 306 + 307 + private function getRequestDirectoryPath() { 308 + $request = $this->getRequest(); 309 + $request_path = $request->getRequestURI()->getPath(); 310 + return preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); 311 + } 312 + 313 + private function authenticateHTTPRepositoryUser( 314 + $username, 315 + PhutilOpaqueEnvelope $password) { 316 + 317 + if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { 318 + // No HTTP auth permitted. 319 + return null; 320 + } 321 + 322 + if (!strlen($username)) { 323 + // No username. 324 + return null; 325 + } 326 + 327 + if (!strlen($password->openEnvelope())) { 328 + // No password. 329 + return null; 330 + } 331 + 332 + $user = id(new PhabricatorPeopleQuery()) 333 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 334 + ->withUsernames(array($username)) 335 + ->executeOne(); 336 + if (!$user) { 337 + // Username doesn't match anything. 338 + return null; 339 + } 340 + 341 + $password_entry = id(new PhabricatorRepositoryVCSPassword()) 342 + ->loadOneWhere('userPHID = %s', $user->getPHID()); 343 + if (!$password_entry) { 344 + // User doesn't have a password set. 345 + return null; 346 + } 347 + 348 + if (!$password_entry->comparePassword($password, $user)) { 349 + // Password doesn't match. 350 + return null; 351 + } 352 + 353 + if ($user->getIsDisabled()) { 354 + // User is disabled. 355 + return null; 356 + } 357 + 358 + return $user; 359 + } 360 + } 361 +