Page MenuHomePhabricator

D15485.id37318.diff
No OneTemporary

D15485.id37318.diff

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
@@ -634,6 +634,7 @@
'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php',
'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php',
'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php',
+ 'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php',
'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php',
'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php',
'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php',
@@ -4763,6 +4764,7 @@
'DiffusionGitBranchTestCase' => 'PhabricatorTestCase',
'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery',
'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow',
+ 'DiffusionGitLFSResponse' => 'AphrontResponse',
'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery',
'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php
--- a/src/applications/diffusion/controller/DiffusionServeController.php
+++ b/src/applications/diffusion/controller/DiffusionServeController.php
@@ -5,6 +5,9 @@
private $serviceViewer;
private $serviceRepository;
+ private $isGitLFSRequest;
+ private $gitLFSToken;
+
public function setServiceViewer(PhabricatorUser $viewer) {
$this->serviceViewer = $viewer;
return $this;
@@ -23,6 +26,14 @@
return $this->serviceRepository;
}
+ public function getIsGitLFSRequest() {
+ return $this->isGitLFSRequest;
+ }
+
+ public function getGitLFSToken() {
+ return $this->gitLFSToken;
+ }
+
public function isVCSRequest(AphrontRequest $request) {
$identifier = $this->getRepositoryIdentifierFromRequest($request);
if ($identifier === null) {
@@ -32,6 +43,9 @@
$content_type = $request->getHTTPHeader('Content-Type');
$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;
if ($request->getExists('service')) {
$service = $request->getStr('service');
@@ -46,6 +60,10 @@
} else if ($content_type == 'application/x-git-receive-pack-request') {
// We get this for `git-receive-pack`.
$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')) {
// Mercurial also sends an Accept header like
// "application/mercurial-0.1", and a User-Agent like
@@ -142,7 +160,17 @@
$username = $_SERVER['PHP_AUTH_USER'];
$password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']);
- $viewer = $this->authenticateHTTPRepositoryUser($username, $password);
+ // 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);
+ }
+
if (!$viewer) {
return new PhabricatorVCSResponse(
403,
@@ -212,46 +240,57 @@
$is_push = !$this->isReadOnlyRequest($repository);
- switch ($repository->getServeOverHTTP()) {
- case PhabricatorRepository::SERVE_READONLY:
- if ($is_push) {
+ 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()) {
+ case PhabricatorRepository::SERVE_READONLY:
+ if ($is_push) {
+ return new PhabricatorVCSResponse(
+ 403,
+ pht('This repository is read-only over HTTP.'));
+ }
+ break;
+ 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 read-only over HTTP.'));
- }
- break;
- case PhabricatorRepository::SERVE_READWRITE:
- if ($is_push) {
- $can_push = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $repository,
- DiffusionPushCapability::CAPABILITY);
- if (!$can_push) {
- if ($viewer->isLoggedIn()) {
- return new PhabricatorVCSResponse(
- 403,
- pht('You do not have permission to push to this repository.'));
- } else {
- 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.'));
- }
- }
+ pht('This repository is not available over HTTP.'));
+ }
+ }
+
+ if ($is_push) {
+ $can_push = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $repository,
+ DiffusionPushCapability::CAPABILITY);
+ if (!$can_push) {
+ if ($viewer->isLoggedIn()) {
+ return new PhabricatorVCSResponse(
+ 403,
+ pht(
+ 'You do not have permission to push to this '.
+ 'repository.'));
+ } else {
+ 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.'));
+ }
}
$vcs_type = $repository->getVersionControlSystem();
@@ -324,6 +363,14 @@
PhabricatorRepository $repository,
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
// to a host which can serve it.
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
@@ -363,6 +410,8 @@
// 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()) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$service = $request->getStr('service');
@@ -514,6 +563,52 @@
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(
$username,
PhutilOpaqueEnvelope $password) {
@@ -739,4 +834,32 @@
);
}
+ 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;
+ }
+
}
diff --git a/src/applications/diffusion/response/DiffusionGitLFSResponse.php b/src/applications/diffusion/response/DiffusionGitLFSResponse.php
new file mode 100644
--- /dev/null
+++ b/src/applications/diffusion/response/DiffusionGitLFSResponse.php
@@ -0,0 +1,37 @@
+<?php
+
+final class DiffusionGitLFSResponse extends AphrontResponse {
+
+ private $content;
+
+ public static function newErrorResponse($code, $message) {
+
+ // We can optionally include "request_id" and "documentation_url" in
+ // this response.
+
+ return id(new self())
+ ->setHTTPResponseCode($code)
+ ->setContent(
+ array(
+ 'message' => $message,
+ ));
+ }
+
+ public function setContent(array $content) {
+ $this->content = phutil_json_encode($content);
+ return $this;
+ }
+
+ public function buildResponseString() {
+ return $this->content;
+ }
+
+ public function getHeaders() {
+ $headers = array(
+ array('Content-Type', 'application/vnd.git-lfs+json'),
+ );
+
+ return array_merge(parent::getHeaders(), $headers);
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
May 13 2024, 2:00 PM (4 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6292944
Default Alt Text
D15485.id37318.diff (11 KB)

Event Timeline