diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index 67d4860318..1fa4a7cff4 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -1,467 +1,567 @@ getRequest(); $uri = $request->getRequestURI(); // Check if this is a VCS request, e.g. from "git clone", "hg clone", or // "svn checkout". If it is, we jump off into repository serving code to // process the request. $regex = '@^/diffusion/(?P[A-Z]+)(/|$)@'; $matches = null; if (preg_match($regex, (string)$uri, $matches)) { $vcs = null; $content_type = $request->getHTTPHeader('Content-Type'); if ($request->getExists('__vcs__')) { // This is magic to make it easier for us to debug stuff by telling // users to run: // // curl http://example.phabricator.com/diffusion/X/?__vcs__=1 // // ...to get a human-readable error. $vcs = $request->getExists('__vcs__'); } else if ($request->getExists('service')) { $service = $request->getStr('service'); // We get this initially for `info/refs`. // Git also gives us a User-Agent like "git/1.8.2.3". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($content_type == 'application/x-git-upload-pack-request') { // We get this for `git-upload-pack`. $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; + } else if ($content_type == 'application/x-git-receive-pack-request') { + // We get this for `git-receive-pack`. + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; } else if ($request->getExists('cmd')) { // Mercurial also sends an Accept header like // "application/mercurial-0.1", and a User-Agent like // "mercurial/proto-1.0". $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; } else { // Subversion also sends an initial OPTIONS request (vs GET/POST), and // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2) // serf/1.3.2". $dav = $request->getHTTPHeader('DAV'); $dav = new PhutilURI($dav); if ($dav->getDomain() === 'subversion.tigris.org') { $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN; } } if ($vcs) { return $this->processVCSRequest($matches['callsign']); } } parent::willBeginExecution(); } private function processVCSRequest($callsign) { - // TODO: Authenticate user. + // If authentication credentials have been provided, try to find a user + // that actually matches those credentials. + if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { + $username = $_SERVER['PHP_AUTH_USER']; + $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); - $viewer = new PhabricatorUser(); + $viewer = $this->authenticateHTTPRepositoryUser($username, $password); + if (!$viewer) { + return new PhabricatorVCSResponse( + 403, + pht('Invalid credentials.')); + } + } else { + // User hasn't provided credentials, which means we count them as + // being "not logged in". + $viewer = new PhabricatorUser(); + } $allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); + $allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); if (!$allow_public) { if (!$viewer->isLoggedIn()) { - return new PhabricatorVCSResponse( - 403, - pht('You must log in to access repositories.')); + if ($allow_auth) { + return new PhabricatorVCSResponse( + 401, + pht('You must log in to access repositories.')); + } else { + return new PhabricatorVCSResponse( + 403, + pht('Public and authenticated HTTP access are both forbidden.')); + } } } try { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withCallsigns(array($callsign)) ->executeOne(); if (!$repository) { return new PhabricatorVCSResponse( 404, pht('No such repository exists.')); } } catch (PhabricatorPolicyException $ex) { if ($viewer->isLoggedIn()) { return new PhabricatorVCSResponse( 403, pht('You do not have permission to access this repository.')); } else { - return new PhabricatorVCSResponse( - 401, - pht('You must log in to access this repository.')); + if ($allow_auth) { + return new PhabricatorVCSResponse( + 401, + pht('You must log in to access this repository.')); + } else { + return new PhabricatorVCSResponse( + 403, + pht( + 'This repository requires authentication, which is forbidden '. + 'over HTTP.')); + } } } if (!$repository->isTracked()) { return new PhabricatorVCSResponse( 403, pht('This repository is inactive.')); } $is_push = !$this->isReadOnlyRequest($repository); switch ($repository->getServeOverHTTP()) { case PhabricatorRepository::SERVE_READONLY: if ($is_push) { return new PhabricatorVCSResponse( 403, pht('This repository is read-only over HTTP.')); } break; case PhabricatorRepository::SERVE_READWRITE: if ($is_push) { $can_push = PhabricatorPolicyFilter::hasCapability( $viewer, $repository, DiffusionCapabilityPush::CAPABILITY); if (!$can_push) { if ($viewer->isLoggedIn()) { return new PhabricatorVCSResponse( 403, pht('You do not have permission to push to this repository.')); } else { - return new PhabricatorVCSResponse( - 401, - pht('You must log in to push to this repository.')); + if ($allow_auth) { + return new PhabricatorVCSResponse( + 401, + pht('You must log in to push to this repository.')); + } else { + return new PhabricatorVCSResponse( + 403, + pht( + 'Pushing to this repository requires authentication, '. + 'which is forbidden over HTTP.')); + } } } } break; case PhabricatorRepository::SERVE_OFF: default: return new PhabricatorVCSResponse( 403, pht('This repository is not available over HTTP.')); } switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $result = $this->serveGitRequest($repository); default: $result = new PhabricatorVCSResponse( 999, pht('TODO: Implement meaningful responses.')); break; } $code = $result->getHTTPResponseCode(); if ($is_push && ($code == 200)) { $repository->writeStatusMessage( PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, PhabricatorRepositoryStatusMessage::CODE_OKAY); } return $result; } private function isReadOnlyRequest( PhabricatorRepository $repository) { $request = $this->getRequest(); $method = $_SERVER['REQUEST_METHOD']; // TODO: This implementation is safe by default, but very incomplete. switch ($repository->getVersionControlSystem()) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $service = $request->getStr('service'); $path = $this->getRequestDirectoryPath(); // NOTE: Service names are the reverse of what you might expect, as they // are from the point of view of the server. The main read service is // "git-upload-pack", and the main write service is "git-receive-pack". if ($method == 'GET' && $path == '/info/refs' && $service == 'git-upload-pack') { return true; } if ($path == '/git-upload-pack') { return true; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $cmd = $request->getStr('cmd'); switch ($cmd) { case 'capabilities': return true; default: return false; } break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION: break; } return false; } public function willProcessRequest(array $data) { if (isset($data['callsign'])) { $drequest = DiffusionRequest::newFromAphrontRequestDictionary( $data, $this->getRequest()); $this->diffusionRequest = $drequest; } } public function setDiffusionRequest(DiffusionRequest $request) { $this->diffusionRequest = $request; return $this; } protected function getDiffusionRequest() { if (!$this->diffusionRequest) { throw new Exception("No Diffusion request object!"); } return $this->diffusionRequest; } public function buildCrumbs(array $spec = array()) { $crumbs = $this->buildApplicationCrumbs(); $crumb_list = $this->buildCrumbList($spec); foreach ($crumb_list as $crumb) { $crumbs->addCrumb($crumb); } return $crumbs; } private function buildCrumbList(array $spec = array()) { $spec = $spec + array( 'commit' => null, 'tags' => null, 'branches' => null, 'view' => null, ); $crumb_list = array(); // On the home page, we don't have a DiffusionRequest. if ($this->diffusionRequest) { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); } else { $drequest = null; $repository = null; } if (!$repository) { return $crumb_list; } $callsign = $repository->getCallsign(); $repository_name = 'r'.$callsign; if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $branch_name = $drequest->getBranch(); if ($branch_name) { $repository_name .= ' ('.$branch_name.')'; } } $crumb = id(new PhabricatorCrumbView()) ->setName($repository_name); if (!$spec['view'] && !$spec['commit'] && !$spec['tags'] && !$spec['branches']) { $crumb_list[] = $crumb; return $crumb_list; } $crumb->setHref( $drequest->generateURI( array( 'action' => 'branch', 'path' => '/', ))); $crumb_list[] = $crumb; $raw_commit = $drequest->getRawCommit(); if ($spec['tags']) { $crumb = new PhabricatorCrumbView(); if ($spec['commit']) { $crumb->setName( pht("Tags for %s", 'r'.$callsign.$raw_commit)); $crumb->setHref($drequest->generateURI( array( 'action' => 'commit', 'commit' => $raw_commit, ))); } else { $crumb->setName(pht('Tags')); } $crumb_list[] = $crumb; return $crumb_list; } if ($spec['branches']) { $crumb = id(new PhabricatorCrumbView()) ->setName(pht('Branches')); $crumb_list[] = $crumb; return $crumb_list; } if ($spec['commit']) { $crumb = id(new PhabricatorCrumbView()) ->setName("r{$callsign}{$raw_commit}") ->setHref("r{$callsign}{$raw_commit}"); $crumb_list[] = $crumb; return $crumb_list; } $crumb = new PhabricatorCrumbView(); $view = $spec['view']; switch ($view) { case 'history': $view_name = pht('History'); break; case 'browse': $view_name = pht('Browse'); break; case 'lint': $view_name = pht('Lint'); break; case 'change': $view_name = pht('Change'); break; } $crumb = id(new PhabricatorCrumbView()) ->setName($view_name); $crumb_list[] = $crumb; return $crumb_list; } protected function callConduitWithDiffusionRequest( $method, array $params = array()) { $user = $this->getRequest()->getUser(); $drequest = $this->getDiffusionRequest(); return DiffusionQuery::callConduitWithDiffusionRequest( $user, $drequest, $method, $params); } protected function getRepositoryControllerURI( PhabricatorRepository $repository, $path) { return $this->getApplicationURI($repository->getCallsign().'/'.$path); } protected function renderPathLinks(DiffusionRequest $drequest, $action) { $path = $drequest->getPath(); $path_parts = array_filter(explode('/', trim($path, '/'))); $divider = phutil_tag( 'span', array( 'class' => 'phui-header-divider'), '/'); $links = array(); if ($path_parts) { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => '', )), ), 'r'.$drequest->getRepository()->getCallsign()); $links[] = $divider; $accum = ''; $last_key = last_key($path_parts); foreach ($path_parts as $key => $part) { $accum .= '/'.$part; if ($key === $last_key) { $links[] = $part; } else { $links[] = phutil_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => $action, 'path' => $accum.'/', )), ), $part); $links[] = $divider; } } } else { $links[] = 'r'.$drequest->getRepository()->getCallsign(); $links[] = $divider; } return $links; } /** * @phutil-external-symbol class PhabricatorStartup */ private function serveGitRequest(PhabricatorRepository $repository) { $request = $this->getRequest(); $request_path = $this->getRequestDirectoryPath(); $repository_root = $repository->getLocalPath(); // Rebuild the query string to strip `__magic__` parameters and prevent // issues where we might interpret inputs like "service=read&service=write" // differently than the server does and pass it an unsafe command. - $query_data = $request->getPassthroughRequestParameters(); + + // NOTE: This does not use getPassthroughRequestParameters() because + // that code is HTTP-method agnostic and will encode POST data. + + $query_data = $_GET; + foreach ($query_data as $key => $value) { + if (!strncmp($key, '__', 2)) { + unset($query_data[$key]); + } + } $query_string = http_build_query($query_data, '', '&'); // We're about to wipe out PATH with the rest of the environment, so // resolve the binary first. $bin = Filesystem::resolveBinary('git-http-backend'); if (!$bin) { throw new Exception("Unable to find `git-http-backend` in PATH!"); } $env = array( 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 'QUERY_STRING' => $query_string, - 'CONTENT_TYPE' => $_SERVER['CONTENT_TYPE'], - 'REMOTE_USER' => '', + 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), + 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 'GIT_PROJECT_ROOT' => $repository_root, 'GIT_HTTP_EXPORT_ALL' => '1', 'PATH_INFO' => $request_path, + + // TODO: Set these correctly. + 'REMOTE_USER' => '', + // GIT_COMMITTER_NAME + // GIT_COMMITTER_EMAIL ); - list($stdout) = id(new ExecFuture('%s', $bin)) + $input = PhabricatorStartup::getRawInput(); + + list($err, $stdout, $stderr) = id(new ExecFuture('%s', $bin)) ->setEnv($env, true) - ->write(PhabricatorStartup::getRawInput()) - ->resolvex(); + ->write($input) + ->resolve(); + + if ($err) { + return new PhabricatorVCSResponse( + 500, + pht('Error %d: %s', $err, $stderr)); + } return id(new DiffusionGitResponse())->setGitData($stdout); } private function getRequestDirectoryPath() { $request = $this->getRequest(); $request_path = $request->getRequestURI()->getPath(); return preg_replace('@^/diffusion/[A-Z]+@', '', $request_path); } protected function renderStatusMessage($title, $body) { return id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setTitle($title) ->appendChild($body); } + + private function authenticateHTTPRepositoryUser( + $username, + PhutilOpaqueEnvelope $password) { + + if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { + // No HTTP auth permitted. + return null; + } + + $user = id(new PhabricatorPeopleQuery()) + ->setViewer(new PhabricatorUser()) + ->withUsernames(array($username)) + ->executeOne(); + if (!$user) { + // Username doesn't match anything. + return null; + } + + $password_entry = id(new PhabricatorRepositoryVCSPassword()) + ->loadOneWhere('userPHID = %s', $user->getPHID()); + if (!$password_entry) { + // User doesn't have a password set. + return null; + } + + if (!$password_entry->comparePassword($password, $user)) { + // Password doesn't match. + return null; + } + + if ($user->getIsDisabled()) { + // User is disabled. + return null; + } + + return $user; + } }