[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 55 return $matches['callsign']; 56 } 57 58 public function processRequest() { 59 $request = $this->getRequest(); 60 $callsign = self::getCallsign($request); 61 62 // If authentication credentials have been provided, try to find a user 63 // that actually matches those credentials. 64 if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { 65 $username = $_SERVER['PHP_AUTH_USER']; 66 $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); 67 68 $viewer = $this->authenticateHTTPRepositoryUser($username, $password); 69 if (!$viewer) { 70 return new PhabricatorVCSResponse( 71 403, 72 pht('Invalid credentials.')); 73 } 74 } else { 75 // User hasn't provided credentials, which means we count them as 76 // being "not logged in". 77 $viewer = new PhabricatorUser(); 78 } 79 80 $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); 81 $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); 82 if (!$allow_public) { 83 if (!$viewer->isLoggedIn()) { 84 if ($allow_auth) { 85 return new PhabricatorVCSResponse( 86 401, 87 pht('You must log in to access repositories.')); 88 } else { 89 return new PhabricatorVCSResponse( 90 403, 91 pht('Public and authenticated HTTP access are both forbidden.')); 92 } 93 } 94 } 95 96 try { 97 $repository = id(new PhabricatorRepositoryQuery()) 98 ->setViewer($viewer) 99 ->withCallsigns(array($callsign)) 100 ->executeOne(); 101 if (!$repository) { 102 return new PhabricatorVCSResponse( 103 404, 104 pht('No such repository exists.')); 105 } 106 } catch (PhabricatorPolicyException $ex) { 107 if ($viewer->isLoggedIn()) { 108 return new PhabricatorVCSResponse( 109 403, 110 pht('You do not have permission to access this repository.')); 111 } else { 112 if ($allow_auth) { 113 return new PhabricatorVCSResponse( 114 401, 115 pht('You must log in to access this repository.')); 116 } else { 117 return new PhabricatorVCSResponse( 118 403, 119 pht( 120 'This repository requires authentication, which is forbidden '. 121 'over HTTP.')); 122 } 123 } 124 } 125 126 if (!$repository->isTracked()) { 127 return new PhabricatorVCSResponse( 128 403, 129 pht('This repository is inactive.')); 130 } 131 132 $is_push = !$this->isReadOnlyRequest($repository); 133 134 switch ($repository->getServeOverHTTP()) { 135 case PhabricatorRepository::SERVE_READONLY: 136 if ($is_push) { 137 return new PhabricatorVCSResponse( 138 403, 139 pht('This repository is read-only over HTTP.')); 140 } 141 break; 142 case PhabricatorRepository::SERVE_READWRITE: 143 if ($is_push) { 144 $can_push = PhabricatorPolicyFilter::hasCapability( 145 $viewer, 146 $repository, 147 DiffusionPushCapability::CAPABILITY); 148 if (!$can_push) { 149 if ($viewer->isLoggedIn()) { 150 return new PhabricatorVCSResponse( 151 403, 152 pht('You do not have permission to push to this repository.')); 153 } else { 154 if ($allow_auth) { 155 return new PhabricatorVCSResponse( 156 401, 157 pht('You must log in to push to this repository.')); 158 } else { 159 return new PhabricatorVCSResponse( 160 403, 161 pht( 162 'Pushing to this repository requires authentication, '. 163 'which is forbidden over HTTP.')); 164 } 165 } 166 } 167 } 168 break; 169 case PhabricatorRepository::SERVE_OFF: 170 default: 171 return new PhabricatorVCSResponse( 172 403, 173 pht('This repository is not available over HTTP.')); 174 } 175 176 $vcs_type = $repository->getVersionControlSystem(); 177 $req_type = $this->isVCSRequest($request); 178 179 if ($vcs_type != $req_type) { 180 switch ($req_type) { 181 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 182 $result = new PhabricatorVCSResponse( 183 500, 184 pht('This is not a Git repository.')); 185 break; 186 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 187 $result = new PhabricatorVCSResponse( 188 500, 189 pht('This is not a Mercurial repository.')); 190 break; 191 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 192 $result = new PhabricatorVCSResponse( 193 500, 194 pht('This is not a Subversion repository.')); 195 break; 196 default: 197 $result = new PhabricatorVCSResponse( 198 500, 199 pht('Unknown request type.')); 200 break; 201 } 202 } else { 203 switch ($vcs_type) { 204 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 205 $result = $this->serveGitRequest($repository, $viewer); 206 break; 207 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 208 $result = $this->serveMercurialRequest($repository, $viewer); 209 break; 210 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 211 $result = new PhabricatorVCSResponse( 212 500, 213 pht( 214 'Phabricator does not support HTTP access to Subversion '. 215 'repositories.')); 216 break; 217 default: 218 $result = new PhabricatorVCSResponse( 219 500, 220 pht('Unknown version control system.')); 221 break; 222 } 223 } 224 225 $code = $result->getHTTPResponseCode(); 226 227 if ($is_push && ($code == 200)) { 228 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 229 $repository->writeStatusMessage( 230 PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, 231 PhabricatorRepositoryStatusMessage::CODE_OKAY); 232 unset($unguarded); 233 } 234 235 return $result; 236 } 237 238 private function isReadOnlyRequest( 239 PhabricatorRepository $repository) { 240 $request = $this->getRequest(); 241 $method = $_SERVER['REQUEST_METHOD']; 242 243 // TODO: This implementation is safe by default, but very incomplete. 244 245 switch ($repository->getVersionControlSystem()) { 246 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 247 $service = $request->getStr('service'); 248 $path = $this->getRequestDirectoryPath($repository); 249 // NOTE: Service names are the reverse of what you might expect, as they 250 // are from the point of view of the server. The main read service is 251 // "git-upload-pack", and the main write service is "git-receive-pack". 252 253 if ($method == 'GET' && 254 $path == '/info/refs' && 255 $service == 'git-upload-pack') { 256 return true; 257 } 258 259 if ($path == '/git-upload-pack') { 260 return true; 261 } 262 263 break; 264 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 265 $cmd = $request->getStr('cmd'); 266 if ($cmd == 'batch') { 267 $cmds = idx($this->getMercurialArguments(), 'cmds'); 268 return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand($cmds); 269 } 270 return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd); 271 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 272 break; 273 } 274 275 return false; 276 } 277 278 /** 279 * @phutil-external-symbol class PhabricatorStartup 280 */ 281 private function serveGitRequest( 282 PhabricatorRepository $repository, 283 PhabricatorUser $viewer) { 284 $request = $this->getRequest(); 285 286 $request_path = $this->getRequestDirectoryPath($repository); 287 $repository_root = $repository->getLocalPath(); 288 289 // Rebuild the query string to strip `__magic__` parameters and prevent 290 // issues where we might interpret inputs like "service=read&service=write" 291 // differently than the server does and pass it an unsafe command. 292 293 // NOTE: This does not use getPassthroughRequestParameters() because 294 // that code is HTTP-method agnostic and will encode POST data. 295 296 $query_data = $_GET; 297 foreach ($query_data as $key => $value) { 298 if (!strncmp($key, '__', 2)) { 299 unset($query_data[$key]); 300 } 301 } 302 $query_string = http_build_query($query_data, '', '&'); 303 304 // We're about to wipe out PATH with the rest of the environment, so 305 // resolve the binary first. 306 $bin = Filesystem::resolveBinary('git-http-backend'); 307 if (!$bin) { 308 throw new Exception('Unable to find `git-http-backend` in PATH!'); 309 } 310 311 $env = array( 312 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 313 'QUERY_STRING' => $query_string, 314 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), 315 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 316 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 317 'GIT_PROJECT_ROOT' => $repository_root, 318 'GIT_HTTP_EXPORT_ALL' => '1', 319 'PATH_INFO' => $request_path, 320 321 'REMOTE_USER' => $viewer->getUsername(), 322 323 // TODO: Set these correctly. 324 // GIT_COMMITTER_NAME 325 // GIT_COMMITTER_EMAIL 326 ) + $this->getCommonEnvironment($viewer); 327 328 $input = PhabricatorStartup::getRawInput(); 329 330 $command = csprintf('%s', $bin); 331 $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); 332 333 list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) 334 ->setEnv($env, true) 335 ->write($input) 336 ->resolve(); 337 338 if ($err) { 339 if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) { 340 // Ignore the error if the response passes this special check for 341 // validity. 342 $err = 0; 343 } 344 } 345 346 if ($err) { 347 return new PhabricatorVCSResponse( 348 500, 349 pht('Error %d: %s', $err, $stderr)); 350 } 351 352 return id(new DiffusionGitResponse())->setGitData($stdout); 353 } 354 355 private function getRequestDirectoryPath(PhabricatorRepository $repository) { 356 $request = $this->getRequest(); 357 $request_path = $request->getRequestURI()->getPath(); 358 $base_path = preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); 359 360 // For Git repositories, strip an optional directory component if it 361 // isn't the name of a known Git resource. This allows users to clone 362 // repositories as "/diffusion/X/anything.git", for example. 363 if ($repository->isGit()) { 364 $known = array( 365 'info', 366 'git-upload-pack', 367 'git-receive-pack', 368 ); 369 370 foreach ($known as $key => $path) { 371 $known[$key] = preg_quote($path, '@'); 372 } 373 374 $known = implode('|', $known); 375 376 if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) { 377 $base_path = preg_replace('@^/([^/]+)@', '', $base_path); 378 } 379 } 380 381 return $base_path; 382 } 383 384 private function authenticateHTTPRepositoryUser( 385 $username, 386 PhutilOpaqueEnvelope $password) { 387 388 if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { 389 // No HTTP auth permitted. 390 return null; 391 } 392 393 if (!strlen($username)) { 394 // No username. 395 return null; 396 } 397 398 if (!strlen($password->openEnvelope())) { 399 // No password. 400 return null; 401 } 402 403 $user = id(new PhabricatorPeopleQuery()) 404 ->setViewer(PhabricatorUser::getOmnipotentUser()) 405 ->withUsernames(array($username)) 406 ->executeOne(); 407 if (!$user) { 408 // Username doesn't match anything. 409 return null; 410 } 411 412 if (!$user->isUserActivated()) { 413 // User is not activated. 414 return null; 415 } 416 417 $password_entry = id(new PhabricatorRepositoryVCSPassword()) 418 ->loadOneWhere('userPHID = %s', $user->getPHID()); 419 if (!$password_entry) { 420 // User doesn't have a password set. 421 return null; 422 } 423 424 if (!$password_entry->comparePassword($password, $user)) { 425 // Password doesn't match. 426 return null; 427 } 428 429 // If the user's password is stored using a less-than-optimal hash, upgrade 430 // them to the strongest available hash. 431 432 $hash_envelope = new PhutilOpaqueEnvelope( 433 $password_entry->getPasswordHash()); 434 if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { 435 $password_entry->setPassword($password, $user); 436 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 437 $password_entry->save(); 438 unset($unguarded); 439 } 440 441 return $user; 442 } 443 444 private function serveMercurialRequest( 445 PhabricatorRepository $repository, 446 PhabricatorUser $viewer) { 447 $request = $this->getRequest(); 448 449 $bin = Filesystem::resolveBinary('hg'); 450 if (!$bin) { 451 throw new Exception('Unable to find `hg` in PATH!'); 452 } 453 454 $env = $this->getCommonEnvironment($viewer); 455 $input = PhabricatorStartup::getRawInput(); 456 457 $cmd = $request->getStr('cmd'); 458 459 $args = $this->getMercurialArguments(); 460 $args = $this->formatMercurialArguments($cmd, $args); 461 462 if (strlen($input)) { 463 $input = strlen($input)."\n".$input."0\n"; 464 } 465 466 $command = csprintf('%s serve --stdio', $bin); 467 $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); 468 469 list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command)) 470 ->setEnv($env, true) 471 ->setCWD($repository->getLocalPath()) 472 ->write("{$cmd}\n{$args}{$input}") 473 ->resolve(); 474 475 if ($err) { 476 return new PhabricatorVCSResponse( 477 500, 478 pht('Error %d: %s', $err, $stderr)); 479 } 480 481 if ($cmd == 'getbundle' || 482 $cmd == 'changegroup' || 483 $cmd == 'changegroupsubset') { 484 // We're not completely sure that "changegroup" and "changegroupsubset" 485 // actually work, they're for very old Mercurial. 486 $body = gzcompress($stdout); 487 } else if ($cmd == 'unbundle') { 488 // This includes diagnostic information and anything echoed by commit 489 // hooks. We ignore `stdout` since it just has protocol garbage, and 490 // substitute `stderr`. 491 $body = strlen($stderr)."\n".$stderr; 492 } else { 493 list($length, $body) = explode("\n", $stdout, 2); 494 } 495 496 return id(new DiffusionMercurialResponse())->setContent($body); 497 } 498 499 500 private function getMercurialArguments() { 501 // Mercurial sends arguments in HTTP headers. "Why?", you might wonder, 502 // "Why would you do this?". 503 504 $args_raw = array(); 505 for ($ii = 1;; $ii++) { 506 $header = 'HTTP_X_HGARG_'.$ii; 507 if (!array_key_exists($header, $_SERVER)) { 508 break; 509 } 510 $args_raw[] = $_SERVER[$header]; 511 } 512 $args_raw = implode('', $args_raw); 513 514 return id(new PhutilQueryStringParser()) 515 ->parseQueryString($args_raw); 516 } 517 518 private function formatMercurialArguments($command, array $arguments) { 519 $spec = DiffusionMercurialWireProtocol::getCommandArgs($command); 520 521 $out = array(); 522 523 // Mercurial takes normal arguments like this: 524 // 525 // name <length(value)> 526 // value 527 528 $has_star = false; 529 foreach ($spec as $arg_key) { 530 if ($arg_key == '*') { 531 $has_star = true; 532 continue; 533 } 534 if (isset($arguments[$arg_key])) { 535 $value = $arguments[$arg_key]; 536 $size = strlen($value); 537 $out[] = "{$arg_key} {$size}\n{$value}"; 538 unset($arguments[$arg_key]); 539 } 540 } 541 542 if ($has_star) { 543 544 // Mercurial takes arguments for variable argument lists roughly like 545 // this: 546 // 547 // * <count(args)> 548 // argname1 <length(argvalue1)> 549 // argvalue1 550 // argname2 <length(argvalue2)> 551 // argvalue2 552 553 $count = count($arguments); 554 555 $out[] = "* {$count}\n"; 556 557 foreach ($arguments as $key => $value) { 558 if (in_array($key, $spec)) { 559 // We already added this argument above, so skip it. 560 continue; 561 } 562 $size = strlen($value); 563 $out[] = "{$key} {$size}\n{$value}"; 564 } 565 } 566 567 return implode('', $out); 568 } 569 570 private function isValidGitShallowCloneResponse($stdout, $stderr) { 571 // If you execute `git clone --depth N ...`, git sends a request which 572 // `git-http-backend` responds to by emitting valid output and then exiting 573 // with a failure code and an error message. If we ignore this error, 574 // everything works. 575 576 // This is a pretty funky fix: it would be nice to more precisely detect 577 // that a request is a `--depth N` clone request, but we don't have any code 578 // to decode protocol frames yet. Instead, look for reasonable evidence 579 // in the error and output that we're looking at a `--depth` clone. 580 581 // For evidence this isn't completely crazy, see: 582 // https://github.com/schacon/grack/pull/7 583 584 $stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m'; 585 $stderr_regexp = '(The remote end hung up unexpectedly)'; 586 587 $has_pack = preg_match($stdout_regexp, $stdout); 588 $is_hangup = preg_match($stderr_regexp, $stderr); 589 590 return $has_pack && $is_hangup; 591 } 592 593 private function getCommonEnvironment(PhabricatorUser $viewer) { 594 $remote_addr = $this->getRequest()->getRemoteAddr(); 595 596 return array( 597 DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), 598 DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_addr, 599 DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', 600 ); 601 } 602 603 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |