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 43 Lines • ▼ Show 20 Lines | try { | ||||
403, | 403, | ||||
pht( | pht( | ||||
'This repository requires authentication, which is forbidden '. | 'This repository requires authentication, which is forbidden '. | ||||
'over HTTP.')); | 'over HTTP.')); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
$response = $this->validateGitLFSRequest($repository, $viewer); | |||||
if ($response) { | |||||
return $response; | |||||
} | |||||
$this->setServiceRepository($repository); | $this->setServiceRepository($repository); | ||||
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. | |||||
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 validateGitLFSRequest( | |||||
PhabricatorRepository $repository, | |||||
PhabricatorUser $viewer) { | |||||
if (!$this->getIsGitLFSRequest()) { | |||||
return null; | |||||
} | |||||
if (!$repository->canUseGitLFS()) { | |||||
return new PhabricatorVCSResponse( | |||||
403, | |||||
pht( | |||||
'The requested repository ("%s") does not support Git LFS.', | |||||
$repository->getDisplayName())); | |||||
} | |||||
// If this is using an LFS token, sanity check that we're using it on the | |||||
// correct repository. This shouldn't really matter since the user could | |||||
// just request a proper token anyway, but it suspicious and should not | |||||
// be permitted. | |||||
$token = $this->getGitLFSToken(); | |||||
if ($token) { | |||||
$resource = $token->getTokenResource(); | |||||
if ($resource !== $repository->getPHID()) { | |||||
return new PhabricatorVCSResponse( | |||||
403, | |||||
pht( | |||||
'The authentication token provided in the request is bound to '. | |||||
'a different repository than the requested repository ("%s").', | |||||
$repository->getDisplayName())); | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
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; | |||||
} | |||||
} | } |