@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement a Git LFS server which supports no operations

Summary:
Ref T7789. This builds on top of `git-lfs-authenticate` to detect LFS requests, read LFS tokens, and route them to a handler which can do useful things.

This handler promptly drops them on the floor with an error message.

Test Plan:
Here's a transcript showing the parts working together so far:

- `git-lfs` connects to the server with SSH, and gets told how to connect with HTTP to do uploads.
- `git-lfs` uses HTTP, and authenticates with the tokens properly.
- But the server tells it to go away, and that it doesn't support anything, so the operation ultimately fails.

```
$ GIT_TRACE=1 git lfs push origin master
12:45:56.153913 git.c:558 trace: exec: 'git-lfs' 'push' 'origin' 'master'
12:45:56.154376 run-command.c:335 trace: run_command: 'git-lfs' 'push' 'origin' 'master'
trace git-lfs: Upload refs origin to remote [master]
trace git-lfs: run_command: git rev-list --objects master --not --remotes=origin
trace git-lfs: run_command: git cat-file --batch-check
trace git-lfs: run_command: git cat-file --batch
trace git-lfs: run_command: 'git' config -l
trace git-lfs: tq: starting 3 transfer workers
trace git-lfs: tq: running as batched queue, batch size of 100
trace git-lfs: prepare upload: b7e0aeb82a03d627c6aa5fc1bbfd454b6789d9d9affc8607d40168fa18cf6c69 lfs/dog1.jpg 1/1
trace git-lfs: tq: sending batch of size 1
trace git-lfs: ssh: local@localvault.phacility.com git-lfs-authenticate diffusion/18/poems.git upload
trace git-lfs: api: batch 1 files
trace git-lfs: HTTP: POST http://local.phacility.com/diffusion/POEMS/poems.git/info/lfs/objects/batch
trace git-lfs: HTTP: 404
trace git-lfs: HTTP: {"message":"Git LFS operation \"objects\/batch\" is not supported by this server."}
trace git-lfs: HTTP:
trace git-lfs: api: batch not implemented: 404
trace git-lfs: run_command: 'git' config lfs.batch false
trace git-lfs: tq: batch api not implemented, falling back to individual
trace git-lfs: ssh: local@localvault.phacility.com git-lfs-authenticate diffusion/18/poems.git upload b7e0aeb82a03d627c6aa5fc1bbfd454b6789d9d9affc8607d40168fa18cf6c69
trace git-lfs: api: uploading (b7e0aeb82a03d627c6aa5fc1bbfd454b6789d9d9affc8607d40168fa18cf6c69)
trace git-lfs: HTTP: POST http://local.phacility.com/diffusion/POEMS/poems.git/info/lfs/objects
trace git-lfs: HTTP: 404
trace git-lfs: HTTP: {"message":"Git LFS operation \"objects\" is not supported by this server."}
trace git-lfs: HTTP:
trace git-lfs: tq: retrying 1 failed transfers
trace git-lfs: ssh: local@localvault.phacility.com git-lfs-authenticate diffusion/18/poems.git upload b7e0aeb82a03d627c6aa5fc1bbfd454b6789d9d9affc8607d40168fa18cf6c69
trace git-lfs: api: uploading (b7e0aeb82a03d627c6aa5fc1bbfd454b6789d9d9affc8607d40168fa18cf6c69)
trace git-lfs: HTTP: POST http://local.phacility.com/diffusion/POEMS/poems.git/info/lfs/objects
trace git-lfs: HTTP: 404
trace git-lfs: HTTP: {"message":"Git LFS operation \"objects\" is not supported by this server."}
trace git-lfs: HTTP:
Git LFS: (0 of 1 files) 0 B / 87.12 KB
Git LFS operation "objects" is not supported by this server.
Git LFS operation "objects" is not supported by this server.
```

Reviewers: chad

Reviewed By: chad

Subscribers: eadler

Maniphest Tasks: T7789

Differential Revision: https://secure.phabricator.com/D15485

+240 -37
+2
src/__phutil_library_map__.php
··· 634 634 'DiffusionGitBranchTestCase' => 'applications/diffusion/data/__tests__/DiffusionGitBranchTestCase.php', 635 635 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 636 636 'DiffusionGitLFSAuthenticateWorkflow' => 'applications/diffusion/gitlfs/DiffusionGitLFSAuthenticateWorkflow.php', 637 + 'DiffusionGitLFSResponse' => 'applications/diffusion/response/DiffusionGitLFSResponse.php', 637 638 'DiffusionGitLFSTemporaryTokenType' => 'applications/diffusion/gitlfs/DiffusionGitLFSTemporaryTokenType.php', 638 639 'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php', 639 640 'DiffusionGitReceivePackSSHWorkflow' => 'applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php', ··· 4763 4764 'DiffusionGitBranchTestCase' => 'PhabricatorTestCase', 4764 4765 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 4765 4766 'DiffusionGitLFSAuthenticateWorkflow' => 'DiffusionGitSSHWorkflow', 4767 + 'DiffusionGitLFSResponse' => 'AphrontResponse', 4766 4768 'DiffusionGitLFSTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 4767 4769 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 4768 4770 'DiffusionGitReceivePackSSHWorkflow' => 'DiffusionGitSSHWorkflow',
+201 -37
src/applications/diffusion/controller/DiffusionServeController.php
··· 5 5 private $serviceViewer; 6 6 private $serviceRepository; 7 7 8 + private $isGitLFSRequest; 9 + private $gitLFSToken; 10 + 8 11 public function setServiceViewer(PhabricatorUser $viewer) { 9 12 $this->serviceViewer = $viewer; 10 13 return $this; ··· 23 26 return $this->serviceRepository; 24 27 } 25 28 29 + public function getIsGitLFSRequest() { 30 + return $this->isGitLFSRequest; 31 + } 32 + 33 + public function getGitLFSToken() { 34 + return $this->gitLFSToken; 35 + } 36 + 26 37 public function isVCSRequest(AphrontRequest $request) { 27 38 $identifier = $this->getRepositoryIdentifierFromRequest($request); 28 39 if ($identifier === null) { ··· 31 42 32 43 $content_type = $request->getHTTPHeader('Content-Type'); 33 44 $user_agent = idx($_SERVER, 'HTTP_USER_AGENT'); 45 + 46 + // This may have a "charset" suffix, so only match the prefix. 47 + $lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))'; 34 48 35 49 $vcs = null; 36 50 if ($request->getExists('service')) { ··· 46 60 } else if ($content_type == 'application/x-git-receive-pack-request') { 47 61 // We get this for `git-receive-pack`. 48 62 $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 63 + } else if (preg_match($lfs_pattern, $content_type)) { 64 + // This is a Git LFS HTTP API request. 65 + $vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT; 66 + $this->isGitLFSRequest = true; 49 67 } else if ($request->getExists('cmd')) { 50 68 // Mercurial also sends an Accept header like 51 69 // "application/mercurial-0.1", and a User-Agent like ··· 142 160 $username = $_SERVER['PHP_AUTH_USER']; 143 161 $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); 144 162 145 - $viewer = $this->authenticateHTTPRepositoryUser($username, $password); 163 + // Try Git LFS auth first since we can usually reject it without doing 164 + // any queries, since the username won't match the one we expect or the 165 + // request won't be LFS. 166 + $viewer = $this->authenticateGitLFSUser($username, $password); 167 + 168 + // If that failed, try normal auth. Note that we can use normal auth on 169 + // LFS requests, so this isn't strictly an alternative to LFS auth. 170 + if (!$viewer) { 171 + $viewer = $this->authenticateHTTPRepositoryUser($username, $password); 172 + } 173 + 146 174 if (!$viewer) { 147 175 return new PhabricatorVCSResponse( 148 176 403, ··· 202 230 } 203 231 } 204 232 233 + $response = $this->validateGitLFSRequest($repository, $viewer); 234 + if ($response) { 235 + return $response; 236 + } 237 + 205 238 $this->setServiceRepository($repository); 206 239 207 240 if (!$repository->isTracked()) { ··· 212 245 213 246 $is_push = !$this->isReadOnlyRequest($repository); 214 247 215 - switch ($repository->getServeOverHTTP()) { 216 - case PhabricatorRepository::SERVE_READONLY: 217 - if ($is_push) { 248 + if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) { 249 + // We allow git LFS requests over HTTP even if the repository does not 250 + // otherwise support HTTP reads or writes, as long as the user is using a 251 + // token from SSH. If they're using HTTP username + password auth, they 252 + // have to obey the normal HTTP rules. 253 + } else { 254 + switch ($repository->getServeOverHTTP()) { 255 + case PhabricatorRepository::SERVE_READONLY: 256 + if ($is_push) { 257 + return new PhabricatorVCSResponse( 258 + 403, 259 + pht('This repository is read-only over HTTP.')); 260 + } 261 + break; 262 + case PhabricatorRepository::SERVE_READWRITE: 263 + // We'll check for push capability below. 264 + break; 265 + case PhabricatorRepository::SERVE_OFF: 266 + default: 267 + return new PhabricatorVCSResponse( 268 + 403, 269 + pht('This repository is not available over HTTP.')); 270 + } 271 + } 272 + 273 + if ($is_push) { 274 + $can_push = PhabricatorPolicyFilter::hasCapability( 275 + $viewer, 276 + $repository, 277 + DiffusionPushCapability::CAPABILITY); 278 + if (!$can_push) { 279 + if ($viewer->isLoggedIn()) { 218 280 return new PhabricatorVCSResponse( 219 281 403, 220 - pht('This repository is read-only over HTTP.')); 221 - } 222 - break; 223 - case PhabricatorRepository::SERVE_READWRITE: 224 - if ($is_push) { 225 - $can_push = PhabricatorPolicyFilter::hasCapability( 226 - $viewer, 227 - $repository, 228 - DiffusionPushCapability::CAPABILITY); 229 - if (!$can_push) { 230 - if ($viewer->isLoggedIn()) { 231 - return new PhabricatorVCSResponse( 232 - 403, 233 - pht('You do not have permission to push to this repository.')); 234 - } else { 235 - if ($allow_auth) { 236 - return new PhabricatorVCSResponse( 237 - 401, 238 - pht('You must log in to push to this repository.')); 239 - } else { 240 - return new PhabricatorVCSResponse( 241 - 403, 242 - pht( 243 - 'Pushing to this repository requires authentication, '. 244 - 'which is forbidden over HTTP.')); 245 - } 246 - } 282 + pht( 283 + 'You do not have permission to push to this '. 284 + 'repository.')); 285 + } else { 286 + if ($allow_auth) { 287 + return new PhabricatorVCSResponse( 288 + 401, 289 + pht('You must log in to push to this repository.')); 290 + } else { 291 + return new PhabricatorVCSResponse( 292 + 403, 293 + pht( 294 + 'Pushing to this repository requires authentication, '. 295 + 'which is forbidden over HTTP.')); 247 296 } 248 297 } 249 - break; 250 - case PhabricatorRepository::SERVE_OFF: 251 - default: 252 - return new PhabricatorVCSResponse( 253 - 403, 254 - pht('This repository is not available over HTTP.')); 298 + } 255 299 } 256 300 257 301 $vcs_type = $repository->getVersionControlSystem(); ··· 324 368 PhabricatorRepository $repository, 325 369 PhabricatorUser $viewer) { 326 370 371 + // We can serve Git LFS requests first, since we don't need to proxy them. 372 + // It's also important that LFS requests never fall through to standard 373 + // service pathways, because that would let you use LFS tokens to read 374 + // normal repository data. 375 + if ($this->getIsGitLFSRequest()) { 376 + return $this->serveGitLFSRequest($repository, $viewer); 377 + } 378 + 327 379 // If this repository is hosted on a service, we need to proxy the request 328 380 // to a host which can serve it. 329 381 $is_cluster_request = $this->getRequest()->isProxiedClusterRequest(); ··· 362 414 $method = $_SERVER['REQUEST_METHOD']; 363 415 364 416 // TODO: This implementation is safe by default, but very incomplete. 417 + 418 + // TODO: This doesn't get the right result for Git LFS yet. 365 419 366 420 switch ($repository->getVersionControlSystem()) { 367 421 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: ··· 514 568 return $base_path; 515 569 } 516 570 571 + private function authenticateGitLFSUser( 572 + $username, 573 + PhutilOpaqueEnvelope $password) { 574 + 575 + // Never accept these credentials for requests which aren't LFS requests. 576 + if (!$this->getIsGitLFSRequest()) { 577 + return null; 578 + } 579 + 580 + // If we have the wrong username, don't bother checking if the token 581 + // is right. 582 + if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) { 583 + return null; 584 + } 585 + 586 + $lfs_pass = $password->openEnvelope(); 587 + $lfs_hash = PhabricatorHash::digest($lfs_pass); 588 + 589 + $token = id(new PhabricatorAuthTemporaryTokenQuery()) 590 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 591 + ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE)) 592 + ->withTokenCodes(array($lfs_hash)) 593 + ->withExpired(false) 594 + ->executeOne(); 595 + if (!$token) { 596 + return null; 597 + } 598 + 599 + $user = id(new PhabricatorPeopleQuery()) 600 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 601 + ->withPHIDs(array($token->getUserPHID())) 602 + ->executeOne(); 603 + 604 + if (!$user) { 605 + return null; 606 + } 607 + 608 + if (!$user->isUserActivated()) { 609 + return null; 610 + } 611 + 612 + $this->gitLFSToken = $token; 613 + 614 + return $user; 615 + } 616 + 517 617 private function authenticateHTTPRepositoryUser( 518 618 $username, 519 619 PhutilOpaqueEnvelope $password) { ··· 737 837 DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address, 738 838 DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http', 739 839 ); 840 + } 841 + 842 + private function validateGitLFSRequest( 843 + PhabricatorRepository $repository, 844 + PhabricatorUser $viewer) { 845 + if (!$this->getIsGitLFSRequest()) { 846 + return null; 847 + } 848 + 849 + if (!$repository->canUseGitLFS()) { 850 + return new PhabricatorVCSResponse( 851 + 403, 852 + pht( 853 + 'The requested repository ("%s") does not support Git LFS.', 854 + $repository->getDisplayName())); 855 + } 856 + 857 + // If this is using an LFS token, sanity check that we're using it on the 858 + // correct repository. This shouldn't really matter since the user could 859 + // just request a proper token anyway, but it suspicious and should not 860 + // be permitted. 861 + 862 + $token = $this->getGitLFSToken(); 863 + if ($token) { 864 + $resource = $token->getTokenResource(); 865 + if ($resource !== $repository->getPHID()) { 866 + return new PhabricatorVCSResponse( 867 + 403, 868 + pht( 869 + 'The authentication token provided in the request is bound to '. 870 + 'a different repository than the requested repository ("%s").', 871 + $repository->getDisplayName())); 872 + } 873 + } 874 + 875 + return null; 876 + } 877 + 878 + private function serveGitLFSRequest( 879 + PhabricatorRepository $repository, 880 + PhabricatorUser $viewer) { 881 + 882 + if (!$this->getIsGitLFSRequest()) { 883 + throw new Exception(pht('This is not a Git LFS request!')); 884 + } 885 + 886 + $path = $this->getGitLFSRequestPath($repository); 887 + 888 + return DiffusionGitLFSResponse::newErrorResponse( 889 + 404, 890 + pht( 891 + 'Git LFS operation "%s" is not supported by this server.', 892 + $path)); 893 + } 894 + 895 + private function getGitLFSRequestPath(PhabricatorRepository $repository) { 896 + $request_path = $this->getRequestDirectoryPath($repository); 897 + 898 + $matches = null; 899 + if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) { 900 + return $matches[1]; 901 + } 902 + 903 + return null; 740 904 } 741 905 742 906 }
+37
src/applications/diffusion/response/DiffusionGitLFSResponse.php
··· 1 + <?php 2 + 3 + final class DiffusionGitLFSResponse extends AphrontResponse { 4 + 5 + private $content; 6 + 7 + public static function newErrorResponse($code, $message) { 8 + 9 + // We can optionally include "request_id" and "documentation_url" in 10 + // this response. 11 + 12 + return id(new self()) 13 + ->setHTTPResponseCode($code) 14 + ->setContent( 15 + array( 16 + 'message' => $message, 17 + )); 18 + } 19 + 20 + public function setContent(array $content) { 21 + $this->content = phutil_json_encode($content); 22 + return $this; 23 + } 24 + 25 + public function buildResponseString() { 26 + return $this->content; 27 + } 28 + 29 + public function getHeaders() { 30 + $headers = array( 31 + array('Content-Type', 'application/vnd.git-lfs+json'), 32 + ); 33 + 34 + return array_merge(parent::getHeaders(), $headers); 35 + } 36 + 37 + }