Changeset View
Changeset View
Standalone View
Standalone View
src/applications/diffusion/controller/DiffusionServeController.php
| <?php | <?php | ||||
| final class DiffusionServeController extends DiffusionController { | final class DiffusionServeController extends DiffusionController { | ||||
| private $serviceViewer; | private $serviceViewer; | ||||
| private $serviceRepository; | private $serviceRepository; | ||||
| private $isGitLFSRequest; | |||||
| private $gitLFSToken; | |||||
| public function setServiceViewer(PhabricatorUser $viewer) { | public function setServiceViewer(PhabricatorUser $viewer) { | ||||
| $this->serviceViewer = $viewer; | $this->serviceViewer = $viewer; | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function getServiceViewer() { | public function getServiceViewer() { | ||||
| return $this->serviceViewer; | return $this->serviceViewer; | ||||
| } | } | ||||
| public function setServiceRepository(PhabricatorRepository $repository) { | public function setServiceRepository(PhabricatorRepository $repository) { | ||||
| $this->serviceRepository = $repository; | $this->serviceRepository = $repository; | ||||
| return $this; | return $this; | ||||
| } | } | ||||
| public function getServiceRepository() { | public function getServiceRepository() { | ||||
| return $this->serviceRepository; | return $this->serviceRepository; | ||||
| } | } | ||||
| public function getIsGitLFSRequest() { | |||||
| return $this->isGitLFSRequest; | |||||
| } | |||||
| public function getGitLFSToken() { | |||||
| return $this->gitLFSToken; | |||||
| } | |||||
| public function isVCSRequest(AphrontRequest $request) { | public function isVCSRequest(AphrontRequest $request) { | ||||
| $identifier = $this->getRepositoryIdentifierFromRequest($request); | $identifier = $this->getRepositoryIdentifierFromRequest($request); | ||||
| if ($identifier === null) { | if ($identifier === null) { | ||||
| return null; | return null; | ||||
| } | } | ||||
| $content_type = $request->getHTTPHeader('Content-Type'); | $content_type = $request->getHTTPHeader('Content-Type'); | ||||
| $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); | $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); | ||||
| // This may have a "charset" suffix, so only match the prefix. | |||||
| $lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))'; | |||||
| $vcs = null; | $vcs = null; | ||||
| if ($request->getExists('service')) { | if ($request->getExists('service')) { | ||||
| $service = $request->getStr('service'); | $service = $request->getStr('service'); | ||||
| // We get this initially for `info/refs`. | // We get this initially for `info/refs`. | ||||
| // Git also gives us a User-Agent like "git/1.8.2.3". | // Git also gives us a User-Agent like "git/1.8.2.3". | ||||
| $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | ||||
| } else if (strncmp($user_agent, 'git/', 4) === 0) { | } else if (strncmp($user_agent, 'git/', 4) === 0) { | ||||
| $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | ||||
| } else if ($content_type == 'application/x-git-upload-pack-request') { | } else if ($content_type == 'application/x-git-upload-pack-request') { | ||||
| // We get this for `git-upload-pack`. | // We get this for `git-upload-pack`. | ||||
| $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | ||||
| } else if ($content_type == 'application/x-git-receive-pack-request') { | } else if ($content_type == 'application/x-git-receive-pack-request') { | ||||
| // We get this for `git-receive-pack`. | // We get this for `git-receive-pack`. | ||||
| $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | ||||
| } else if (preg_match($lfs_pattern, $content_type)) { | |||||
| // This is a Git LFS HTTP API request. | |||||
| $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; | |||||
| $this->isGitLFSRequest = true; | |||||
| } else if ($request->getExists('cmd')) { | } else if ($request->getExists('cmd')) { | ||||
| // Mercurial also sends an Accept header like | // Mercurial also sends an Accept header like | ||||
| // "application/mercurial-0.1", and a User-Agent like | // "application/mercurial-0.1", and a User-Agent like | ||||
| // "mercurial/proto-1.0". | // "mercurial/proto-1.0". | ||||
| $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; | $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL; | ||||
| } else { | } else { | ||||
| // Subversion also sends an initial OPTIONS request (vs GET/POST), and | // 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) | // has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2) | ||||
| ▲ Show 20 Lines • Show All 80 Lines • ▼ Show 20 Lines | private function serveRequest(AphrontRequest $request) { | ||||
| $identifier = $this->getRepositoryIdentifierFromRequest($request); | $identifier = $this->getRepositoryIdentifierFromRequest($request); | ||||
| // If authentication credentials have been provided, try to find a user | // If authentication credentials have been provided, try to find a user | ||||
| // that actually matches those credentials. | // that actually matches those credentials. | ||||
| if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { | if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { | ||||
| $username = $_SERVER['PHP_AUTH_USER']; | $username = $_SERVER['PHP_AUTH_USER']; | ||||
| $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); | $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); | ||||
| // Try Git LFS auth first since we can usually reject it without doing | |||||
| // any queries, since the username won't match the one we expect or the | |||||
| // request won't be LFS. | |||||
| $viewer = $this->authenticateGitLFSUser($username, $password); | |||||
| // If that failed, try normal auth. Note that we can use normal auth on | |||||
| // LFS requests, so this isn't strictly an alternative to LFS auth. | |||||
| if (!$viewer) { | |||||
| $viewer = $this->authenticateHTTPRepositoryUser($username, $password); | $viewer = $this->authenticateHTTPRepositoryUser($username, $password); | ||||
| } | |||||
| if (!$viewer) { | if (!$viewer) { | ||||
| return new PhabricatorVCSResponse( | return new PhabricatorVCSResponse( | ||||
| 403, | 403, | ||||
| pht('Invalid credentials.')); | pht('Invalid credentials.')); | ||||
| } | } | ||||
| } else { | } else { | ||||
| // User hasn't provided credentials, which means we count them as | // User hasn't provided credentials, which means we count them as | ||||
| // being "not logged in". | // being "not logged in". | ||||
| ▲ Show 20 Lines • Show All 53 Lines • ▼ Show 20 Lines | private function serveRequest(AphrontRequest $request) { | ||||
| if (!$repository->isTracked()) { | if (!$repository->isTracked()) { | ||||
| return new PhabricatorVCSResponse( | return new PhabricatorVCSResponse( | ||||
| 403, | 403, | ||||
| pht('This repository is inactive.')); | pht('This repository is inactive.')); | ||||
| } | } | ||||
| $is_push = !$this->isReadOnlyRequest($repository); | $is_push = !$this->isReadOnlyRequest($repository); | ||||
| if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) { | |||||
| // We allow git LFS requests over HTTP even if the repository does not | |||||
| // otherwise support HTTP reads or writes, as long as the user is using a | |||||
| // token from SSH. If they're using HTTP username + password auth, they | |||||
| // have to obey the normal HTTP rules. | |||||
| } else { | |||||
| switch ($repository->getServeOverHTTP()) { | switch ($repository->getServeOverHTTP()) { | ||||
| case PhabricatorRepository::SERVE_READONLY: | case PhabricatorRepository::SERVE_READONLY: | ||||
| if ($is_push) { | if ($is_push) { | ||||
| return new PhabricatorVCSResponse( | return new PhabricatorVCSResponse( | ||||
| 403, | 403, | ||||
| pht('This repository is read-only over HTTP.')); | pht('This repository is read-only over HTTP.')); | ||||
| } | } | ||||
| break; | break; | ||||
| case PhabricatorRepository::SERVE_READWRITE: | case PhabricatorRepository::SERVE_READWRITE: | ||||
| // We'll check for push capability below. | |||||
| break; | |||||
| case PhabricatorRepository::SERVE_OFF: | |||||
| default: | |||||
| return new PhabricatorVCSResponse( | |||||
| 403, | |||||
| pht('This repository is not available over HTTP.')); | |||||
| } | |||||
| } | |||||
| if ($is_push) { | if ($is_push) { | ||||
| $can_push = PhabricatorPolicyFilter::hasCapability( | $can_push = PhabricatorPolicyFilter::hasCapability( | ||||
| $viewer, | $viewer, | ||||
| $repository, | $repository, | ||||
| DiffusionPushCapability::CAPABILITY); | DiffusionPushCapability::CAPABILITY); | ||||
| if (!$can_push) { | if (!$can_push) { | ||||
| if ($viewer->isLoggedIn()) { | if ($viewer->isLoggedIn()) { | ||||
| return new PhabricatorVCSResponse( | return new PhabricatorVCSResponse( | ||||
| 403, | 403, | ||||
| pht('You do not have permission to push to this repository.')); | pht( | ||||
| 'You do not have permission to push to this '. | |||||
| 'repository.')); | |||||
| } else { | } else { | ||||
| if ($allow_auth) { | if ($allow_auth) { | ||||
| return new PhabricatorVCSResponse( | return new PhabricatorVCSResponse( | ||||
| 401, | 401, | ||||
| pht('You must log in to push to this repository.')); | pht('You must log in to push to this repository.')); | ||||
| } else { | } else { | ||||
| return new PhabricatorVCSResponse( | return new PhabricatorVCSResponse( | ||||
| 403, | 403, | ||||
| pht( | pht( | ||||
| 'Pushing to this repository requires authentication, '. | 'Pushing to this repository requires authentication, '. | ||||
| 'which is forbidden over HTTP.')); | 'which is forbidden over HTTP.')); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| break; | |||||
| case PhabricatorRepository::SERVE_OFF: | |||||
| default: | |||||
| return new PhabricatorVCSResponse( | |||||
| 403, | |||||
| pht('This repository is not available over HTTP.')); | |||||
| } | |||||
| $vcs_type = $repository->getVersionControlSystem(); | $vcs_type = $repository->getVersionControlSystem(); | ||||
| $req_type = $this->isVCSRequest($request); | $req_type = $this->isVCSRequest($request); | ||||
| if ($vcs_type != $req_type) { | if ($vcs_type != $req_type) { | ||||
| switch ($req_type) { | switch ($req_type) { | ||||
| case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: | case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: | ||||
| $result = new PhabricatorVCSResponse( | $result = new PhabricatorVCSResponse( | ||||
| ▲ Show 20 Lines • Show All 55 Lines • ▼ Show 20 Lines | private function serveRequest(AphrontRequest $request) { | ||||
| return $result; | return $result; | ||||
| } | } | ||||
| private function serveVCSRequest( | private function serveVCSRequest( | ||||
| PhabricatorRepository $repository, | PhabricatorRepository $repository, | ||||
| PhabricatorUser $viewer) { | PhabricatorUser $viewer) { | ||||
| // We can serve Git LFS requests first, since we don't need to proxy them. | |||||
| // It's also important that LFS requests never fall through to standard | |||||
| // service pathways, because that would let you use LFS tokens to read | |||||
| // normal repository data. | |||||
| if ($this->getIsGitLFSRequest()) { | |||||
| return $this->serveGitLFSRequest($repository, $viewer); | |||||
| } | |||||
| // If this repository is hosted on a service, we need to proxy the request | // If this repository is hosted on a service, we need to proxy the request | ||||
| // to a host which can serve it. | // to a host which can serve it. | ||||
| $is_cluster_request = $this->getRequest()->isProxiedClusterRequest(); | $is_cluster_request = $this->getRequest()->isProxiedClusterRequest(); | ||||
| $uri = $repository->getAlmanacServiceURI( | $uri = $repository->getAlmanacServiceURI( | ||||
| $viewer, | $viewer, | ||||
| $is_cluster_request, | $is_cluster_request, | ||||
| array( | array( | ||||
| Show All 23 Lines | final class DiffusionServeController extends DiffusionController { | ||||
| private function isReadOnlyRequest( | private function isReadOnlyRequest( | ||||
| PhabricatorRepository $repository) { | PhabricatorRepository $repository) { | ||||
| $request = $this->getRequest(); | $request = $this->getRequest(); | ||||
| $method = $_SERVER['REQUEST_METHOD']; | $method = $_SERVER['REQUEST_METHOD']; | ||||
| // TODO: This implementation is safe by default, but very incomplete. | // TODO: This implementation is safe by default, but very incomplete. | ||||
| // TODO: This doesn't get the right result for Git LFS yet. | |||||
Lint: TODO Comment: This comment has a TODO. | |||||
| switch ($repository->getVersionControlSystem()) { | switch ($repository->getVersionControlSystem()) { | ||||
| case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: | case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: | ||||
| $service = $request->getStr('service'); | $service = $request->getStr('service'); | ||||
| $path = $this->getRequestDirectoryPath($repository); | $path = $this->getRequestDirectoryPath($repository); | ||||
| // NOTE: Service names are the reverse of what you might expect, as they | // 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 | // 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". | // "git-upload-pack", and the main write service is "git-receive-pack". | ||||
| ▲ Show 20 Lines • Show All 135 Lines • ▼ Show 20 Lines | if ($repository->isGit()) { | ||||
| if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) { | if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) { | ||||
| $base_path = preg_replace('@^/([^/]+)@', '', $base_path); | $base_path = preg_replace('@^/([^/]+)@', '', $base_path); | ||||
| } | } | ||||
| } | } | ||||
| return $base_path; | return $base_path; | ||||
| } | } | ||||
| private function authenticateGitLFSUser( | |||||
| $username, | |||||
| PhutilOpaqueEnvelope $password) { | |||||
| // Never accept these credentials for requests which aren't LFS requests. | |||||
| if (!$this->getIsGitLFSRequest()) { | |||||
| return null; | |||||
| } | |||||
| // If we have the wrong username, don't bother checking if the token | |||||
| // is right. | |||||
| if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) { | |||||
| return null; | |||||
| } | |||||
| $lfs_pass = $password->openEnvelope(); | |||||
| $lfs_hash = PhabricatorHash::digest($lfs_pass); | |||||
| $token = id(new PhabricatorAuthTemporaryTokenQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE)) | |||||
| ->withTokenCodes(array($lfs_hash)) | |||||
| ->withExpired(false) | |||||
| ->executeOne(); | |||||
| if (!$token) { | |||||
| return null; | |||||
| } | |||||
| $user = id(new PhabricatorPeopleQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withPHIDs(array($token->getUserPHID())) | |||||
| ->executeOne(); | |||||
| if (!$user) { | |||||
| return null; | |||||
| } | |||||
| if (!$user->isUserActivated()) { | |||||
| return null; | |||||
| } | |||||
| $this->gitLFSToken = $token; | |||||
| return $user; | |||||
| } | |||||
| private function authenticateHTTPRepositoryUser( | private function authenticateHTTPRepositoryUser( | ||||
| $username, | $username, | ||||
| PhutilOpaqueEnvelope $password) { | PhutilOpaqueEnvelope $password) { | ||||
| if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { | if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) { | ||||
| // No HTTP auth permitted. | // No HTTP auth permitted. | ||||
| return null; | return null; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 209 Lines • ▼ Show 20 Lines | private function getCommonEnvironment(PhabricatorUser $viewer) { | ||||
| return array( | return array( | ||||
| DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), | DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(), | ||||
| DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address, | DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address, | ||||
| DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', | DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', | ||||
| ); | ); | ||||
| } | } | ||||
| private function serveGitLFSRequest( | |||||
| PhabricatorRepository $repository, | |||||
| PhabricatorUser $viewer) { | |||||
| if (!$this->getIsGitLFSRequest()) { | |||||
| throw new Exception(pht('This is not a Git LFS request!')); | |||||
| } | |||||
| $path = $this->getGitLFSRequestPath($repository); | |||||
| return DiffusionGitLFSResponse::newErrorResponse( | |||||
| 404, | |||||
| pht( | |||||
| 'Git LFS operation "%s" is not supported by this server.', | |||||
| $path)); | |||||
| } | |||||
| private function getGitLFSRequestPath(PhabricatorRepository $repository) { | |||||
| $request_path = $this->getRequestDirectoryPath($repository); | |||||
| $matches = null; | |||||
| if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) { | |||||
| return $matches[1]; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| } | } | ||||
This comment has a TODO.