diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -633,6 +633,8 @@ 'DiffusionGitBranch' => 'applications/diffusion/data/DiffusionGitBranch.php', 'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php', 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', + 'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php', + 'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php', 'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php', 'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php', 'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php', @@ -4760,6 +4762,8 @@ 'DiffusionGitBranch' => 'Phobject', 'DiffusionGitBranchTestCase' => 'PhabricatorTestCase', 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', + 'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow', + 'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow', 'DiffusionGitRequest' => 'DiffusionRequest', diff --git a/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php @@ -0,0 +1,118 @@ +setName('git-lfs-authenticate'); + $this->setArguments( + array( + array( + 'name' => 'argv', + 'wildcard' => true, + ), + )); + } + + protected function identifyRepository() { + return $this->loadRepositoryWithPath($this->getLFSPathArgument()); + } + + private function getLFSPathArgument() { + return $this->getLFSArgument(0); + } + + private function getLFSOperationArgument() { + return $this->getLFSArgument(1); + } + + private function getLFSArgument($position) { + $args = $this->getArgs(); + $argv = $args->getArg('argv'); + + if (!isset($argv[$position])) { + throw new Exception( + pht( + 'Expected `git-lfs-authenticate `, but received '. + 'too few arguments.')); + } + + return $argv[$position]; + } + + protected function executeRepositoryOperations() { + $operation = $this->getLFSOperationArgument(); + + // NOTE: We aren't checking write access here, even for "upload". The + // HTTP endpoint should be able to do that for us. + + switch ($operation) { + case 'upload': + case 'download': + break; + default: + throw new Exception( + pht( + 'Git LFS operation "%s" is not supported by this server.', + $operation)); + } + + $repository = $this->getRepository(); + + if (!$repository->isGit()) { + throw new Exception( + pht( + 'Repository "%s" is not a Git repository. Git LFS is only '. + 'supported for Git repositories.', + $repository->getDisplayName())); + } + + if (!$repository->canUseGitLFS()) { + throw new Exception( + pht('Git LFS is not enabled for this repository.')); + } + + // NOTE: This is usually the same as the default URI (which does not + // need to be specified in the response), but the protocol or domain may + // differ in some situations. + + $lfs_uri = $repository->getGitLFSURI('info/lfs'); + + // Generate a temporary token to allow the user to acces LFS over HTTP. + // This works even if normal HTTP repository operations are not available + // on this host, and does not require the user to have a VCS password. + + $user = $this->getUser(); + $headers = array(); + + $lfs_user = DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME; + $lfs_pass = Filesystem::readRandomCharacters(32); + $lfs_hash = PhabricatorHash::digest($lfs_pass); + + $ttl = PhabricatorTime::getNow() + phutil_units('1 day in seconds'); + + $token = id(new PhabricatorAuthTemporaryToken()) + ->setTokenResource($repository->getPHID()) + ->setTokenType(DiffusionGitLFSTemporaryTokenType::TOKENTYPE) + ->setTokenCode($lfs_hash) + ->setUserPHID($user->getPHID()) + ->setTemporaryTokenProperty('lfs.operation', $operation) + ->setTokenExpires($ttl) + ->save(); + + $authorization_header = base64_encode($lfs_user.':'.$lfs_pass); + $headers['Authorization'] = 'Basic '.$authorization_header; + + $result = array( + 'header' => $headers, + 'href' => $lfs_uri, + ); + $result = phutil_json_encode($result); + + $this->writeIO($result); + $this->waitForGitClient(); + + return 0; + } + +} diff --git a/src/applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php b/src/applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php @@ -0,0 +1,18 @@ +getRawHTTPCloneURIObject(); + } + + private function getRawHTTPCloneURIObject() { $uri = PhabricatorEnv::getProductionURI($this->getURI()); $uri = new PhutilURI($uri); @@ -1819,6 +1823,38 @@ return !$this->isSVN(); } + public function canUseGitLFS() { + if (!$this->isGit()) { + return false; + } + + if (!$this->isHosted()) { + return false; + } + + // TODO: Unprototype this feature. + if (!PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { + return false; + } + + return true; + } + + public function getGitLFSURI($path = null) { + if (!$this->canUseGitLFS()) { + throw new Exception( + pht( + 'This repository does not support Git LFS, so Git LFS URIs can '. + 'not be generated for it.')); + } + + $uri = $this->getRawHTTPCloneURIObject(); + $uri = (string)$uri; + $uri = $uri.'/'.$path; + + return $uri; + } + public function canMirror() { if ($this->isGit() || $this->isHg()) { return true;