Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/controller/DiffusionServeController.php
12242 views
1
<?php
2
3
final class DiffusionServeController extends DiffusionController {
4
5
private $serviceViewer;
6
private $serviceRepository;
7
8
private $isGitLFSRequest;
9
private $gitLFSToken;
10
private $gitLFSInput;
11
12
public function setServiceViewer(PhabricatorUser $viewer) {
13
$this->getRequest()->setUser($viewer);
14
15
$this->serviceViewer = $viewer;
16
return $this;
17
}
18
19
public function getServiceViewer() {
20
return $this->serviceViewer;
21
}
22
23
public function setServiceRepository(PhabricatorRepository $repository) {
24
$this->serviceRepository = $repository;
25
return $this;
26
}
27
28
public function getServiceRepository() {
29
return $this->serviceRepository;
30
}
31
32
public function getIsGitLFSRequest() {
33
return $this->isGitLFSRequest;
34
}
35
36
public function getGitLFSToken() {
37
return $this->gitLFSToken;
38
}
39
40
public function isVCSRequest(AphrontRequest $request) {
41
$identifier = $this->getRepositoryIdentifierFromRequest($request);
42
if ($identifier === null) {
43
return null;
44
}
45
46
$content_type = $request->getHTTPHeader('Content-Type');
47
$user_agent = idx($_SERVER, 'HTTP_USER_AGENT');
48
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
49
50
// This may have a "charset" suffix, so only match the prefix.
51
$lfs_pattern = '(^application/vnd\\.git-lfs\\+json(;|\z))';
52
53
$vcs = null;
54
if ($request->getExists('service')) {
55
$service = $request->getStr('service');
56
// We get this initially for `info/refs`.
57
// Git also gives us a User-Agent like "git/1.8.2.3".
58
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
59
} else if (strncmp($user_agent, 'git/', 4) === 0) {
60
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
61
} else if ($content_type == 'application/x-git-upload-pack-request') {
62
// We get this for `git-upload-pack`.
63
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
64
} else if ($content_type == 'application/x-git-receive-pack-request') {
65
// We get this for `git-receive-pack`.
66
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
67
} else if (preg_match($lfs_pattern, $content_type)) {
68
// This is a Git LFS HTTP API request.
69
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
70
$this->isGitLFSRequest = true;
71
} else if ($request_type == 'git-lfs') {
72
// This is a Git LFS object content request.
73
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
74
$this->isGitLFSRequest = true;
75
} else if ($request->getExists('cmd')) {
76
// Mercurial also sends an Accept header like
77
// "application/mercurial-0.1", and a User-Agent like
78
// "mercurial/proto-1.0".
79
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
80
} else {
81
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
82
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
83
// serf/1.3.2".
84
$dav = $request->getHTTPHeader('DAV');
85
$dav = new PhutilURI($dav);
86
if ($dav->getDomain() === 'subversion.tigris.org') {
87
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
88
}
89
}
90
91
return $vcs;
92
}
93
94
public function handleRequest(AphrontRequest $request) {
95
$service_exception = null;
96
$response = null;
97
98
try {
99
$response = $this->serveRequest($request);
100
} catch (Exception $ex) {
101
$service_exception = $ex;
102
}
103
104
try {
105
$remote_addr = $request->getRemoteAddress();
106
107
if ($request->isHTTPS()) {
108
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTPS;
109
} else {
110
$remote_protocol = PhabricatorRepositoryPullEvent::PROTOCOL_HTTP;
111
}
112
113
$pull_event = id(new PhabricatorRepositoryPullEvent())
114
->setEpoch(PhabricatorTime::getNow())
115
->setRemoteAddress($remote_addr)
116
->setRemoteProtocol($remote_protocol);
117
118
if ($response) {
119
$response_code = $response->getHTTPResponseCode();
120
121
if ($response_code == 200) {
122
$pull_event
123
->setResultType(PhabricatorRepositoryPullEvent::RESULT_PULL)
124
->setResultCode($response_code);
125
} else {
126
$pull_event
127
->setResultType(PhabricatorRepositoryPullEvent::RESULT_ERROR)
128
->setResultCode($response_code);
129
}
130
131
if ($response instanceof PhabricatorVCSResponse) {
132
$pull_event->setProperties(
133
array(
134
'response.message' => $response->getMessage(),
135
));
136
}
137
} else {
138
$pull_event
139
->setResultType(PhabricatorRepositoryPullEvent::RESULT_EXCEPTION)
140
->setResultCode(500)
141
->setProperties(
142
array(
143
'exception.class' => get_class($ex),
144
'exception.message' => $ex->getMessage(),
145
));
146
}
147
148
$viewer = $this->getServiceViewer();
149
if ($viewer) {
150
$pull_event->setPullerPHID($viewer->getPHID());
151
}
152
153
$repository = $this->getServiceRepository();
154
if ($repository) {
155
$pull_event->setRepositoryPHID($repository->getPHID());
156
}
157
158
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
159
$pull_event->save();
160
unset($unguarded);
161
162
} catch (Exception $ex) {
163
if ($service_exception) {
164
throw $service_exception;
165
}
166
throw $ex;
167
}
168
169
if ($service_exception) {
170
throw $service_exception;
171
}
172
173
return $response;
174
}
175
176
private function serveRequest(AphrontRequest $request) {
177
$identifier = $this->getRepositoryIdentifierFromRequest($request);
178
179
// If authentication credentials have been provided, try to find a user
180
// that actually matches those credentials.
181
182
// We require both the username and password to be nonempty, because Git
183
// won't prompt users who provide a username but no password otherwise.
184
// See T10797 for discussion.
185
186
$http_user = idx($_SERVER, 'PHP_AUTH_USER');
187
$http_pass = idx($_SERVER, 'PHP_AUTH_PW');
188
$have_user = $http_user !== null && strlen($http_user);
189
$have_pass = $http_pass !== null && strlen($http_pass);
190
if ($have_user && $have_pass) {
191
$username = $http_user;
192
$password = new PhutilOpaqueEnvelope($http_pass);
193
194
// Try Git LFS auth first since we can usually reject it without doing
195
// any queries, since the username won't match the one we expect or the
196
// request won't be LFS.
197
$viewer = $this->authenticateGitLFSUser(
198
$username,
199
$password,
200
$identifier);
201
202
// If that failed, try normal auth. Note that we can use normal auth on
203
// LFS requests, so this isn't strictly an alternative to LFS auth.
204
if (!$viewer) {
205
$viewer = $this->authenticateHTTPRepositoryUser($username, $password);
206
}
207
208
if (!$viewer) {
209
return new PhabricatorVCSResponse(
210
403,
211
pht('Invalid credentials.'));
212
}
213
} else {
214
// User hasn't provided credentials, which means we count them as
215
// being "not logged in".
216
$viewer = new PhabricatorUser();
217
}
218
219
// See T13590. Some pathways, like error handling, may require unusual
220
// access to things like timezone information. These are fine to build
221
// inline; this pathway is not lightweight anyway.
222
$viewer->setAllowInlineCacheGeneration(true);
223
224
$this->setServiceViewer($viewer);
225
226
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
227
$allow_auth = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth');
228
if (!$allow_public) {
229
if (!$viewer->isLoggedIn()) {
230
if ($allow_auth) {
231
return new PhabricatorVCSResponse(
232
401,
233
pht('You must log in to access repositories.'));
234
} else {
235
return new PhabricatorVCSResponse(
236
403,
237
pht('Public and authenticated HTTP access are both forbidden.'));
238
}
239
}
240
}
241
242
try {
243
$repository = id(new PhabricatorRepositoryQuery())
244
->setViewer($viewer)
245
->withIdentifiers(array($identifier))
246
->needURIs(true)
247
->executeOne();
248
if (!$repository) {
249
return new PhabricatorVCSResponse(
250
404,
251
pht('No such repository exists.'));
252
}
253
} catch (PhabricatorPolicyException $ex) {
254
if ($viewer->isLoggedIn()) {
255
return new PhabricatorVCSResponse(
256
403,
257
pht('You do not have permission to access this repository.'));
258
} else {
259
if ($allow_auth) {
260
return new PhabricatorVCSResponse(
261
401,
262
pht('You must log in to access this repository.'));
263
} else {
264
return new PhabricatorVCSResponse(
265
403,
266
pht(
267
'This repository requires authentication, which is forbidden '.
268
'over HTTP.'));
269
}
270
}
271
}
272
273
$response = $this->validateGitLFSRequest($repository, $viewer);
274
if ($response) {
275
return $response;
276
}
277
278
$this->setServiceRepository($repository);
279
280
if (!$repository->isTracked()) {
281
return new PhabricatorVCSResponse(
282
403,
283
pht('This repository is inactive.'));
284
}
285
286
$is_push = !$this->isReadOnlyRequest($repository);
287
288
if ($this->getIsGitLFSRequest() && $this->getGitLFSToken()) {
289
// We allow git LFS requests over HTTP even if the repository does not
290
// otherwise support HTTP reads or writes, as long as the user is using a
291
// token from SSH. If they're using HTTP username + password auth, they
292
// have to obey the normal HTTP rules.
293
} else {
294
// For now, we don't distinguish between HTTP and HTTPS-originated
295
// requests that are proxied within the cluster, so the user can connect
296
// with HTTPS but we may be on HTTP by the time we reach this part of
297
// the code. Allow things to move forward as long as either protocol
298
// can be served.
299
$proto_https = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS;
300
$proto_http = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP;
301
302
$can_read =
303
$repository->canServeProtocol($proto_https, false) ||
304
$repository->canServeProtocol($proto_http, false);
305
if (!$can_read) {
306
return new PhabricatorVCSResponse(
307
403,
308
pht('This repository is not available over HTTP.'));
309
}
310
311
if ($is_push) {
312
if ($repository->isReadOnly()) {
313
return new PhabricatorVCSResponse(
314
503,
315
$repository->getReadOnlyMessageForDisplay());
316
}
317
318
$can_write =
319
$repository->canServeProtocol($proto_https, true) ||
320
$repository->canServeProtocol($proto_http, true);
321
if (!$can_write) {
322
return new PhabricatorVCSResponse(
323
403,
324
pht('This repository is read-only over HTTP.'));
325
}
326
}
327
}
328
329
if ($is_push) {
330
$can_push = PhabricatorPolicyFilter::hasCapability(
331
$viewer,
332
$repository,
333
DiffusionPushCapability::CAPABILITY);
334
if (!$can_push) {
335
if ($viewer->isLoggedIn()) {
336
$error_code = 403;
337
$error_message = pht(
338
'You do not have permission to push to this repository ("%s").',
339
$repository->getDisplayName());
340
341
if ($this->getIsGitLFSRequest()) {
342
return DiffusionGitLFSResponse::newErrorResponse(
343
$error_code,
344
$error_message);
345
} else {
346
return new PhabricatorVCSResponse(
347
$error_code,
348
$error_message);
349
}
350
} else {
351
if ($allow_auth) {
352
return new PhabricatorVCSResponse(
353
401,
354
pht('You must log in to push to this repository.'));
355
} else {
356
return new PhabricatorVCSResponse(
357
403,
358
pht(
359
'Pushing to this repository requires authentication, '.
360
'which is forbidden over HTTP.'));
361
}
362
}
363
}
364
}
365
366
$vcs_type = $repository->getVersionControlSystem();
367
$req_type = $this->isVCSRequest($request);
368
369
if ($vcs_type != $req_type) {
370
switch ($req_type) {
371
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
372
$result = new PhabricatorVCSResponse(
373
500,
374
pht(
375
'This repository ("%s") is not a Git repository.',
376
$repository->getDisplayName()));
377
break;
378
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
379
$result = new PhabricatorVCSResponse(
380
500,
381
pht(
382
'This repository ("%s") is not a Mercurial repository.',
383
$repository->getDisplayName()));
384
break;
385
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
386
$result = new PhabricatorVCSResponse(
387
500,
388
pht(
389
'This repository ("%s") is not a Subversion repository.',
390
$repository->getDisplayName()));
391
break;
392
default:
393
$result = new PhabricatorVCSResponse(
394
500,
395
pht('Unknown request type.'));
396
break;
397
}
398
} else {
399
switch ($vcs_type) {
400
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
401
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
402
$caught = null;
403
try {
404
$result = $this->serveVCSRequest($repository, $viewer);
405
} catch (Exception $ex) {
406
$caught = $ex;
407
} catch (Throwable $ex) {
408
$caught = $ex;
409
}
410
411
if ($caught) {
412
// We never expect an uncaught exception here, so dump it to the
413
// log. All routine errors should have been converted into Response
414
// objects by a lower layer.
415
phlog($caught);
416
417
$result = new PhabricatorVCSResponse(
418
500,
419
phutil_string_cast($caught->getMessage()));
420
}
421
break;
422
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
423
$result = new PhabricatorVCSResponse(
424
500,
425
pht(
426
'This server does not support HTTP access to Subversion '.
427
'repositories.'));
428
break;
429
default:
430
$result = new PhabricatorVCSResponse(
431
500,
432
pht('Unknown version control system.'));
433
break;
434
}
435
}
436
437
$code = $result->getHTTPResponseCode();
438
439
if ($is_push && ($code == 200)) {
440
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
441
$repository->writeStatusMessage(
442
PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE,
443
PhabricatorRepositoryStatusMessage::CODE_OKAY);
444
unset($unguarded);
445
}
446
447
return $result;
448
}
449
450
private function serveVCSRequest(
451
PhabricatorRepository $repository,
452
PhabricatorUser $viewer) {
453
454
// We can serve Git LFS requests first, since we don't need to proxy them.
455
// It's also important that LFS requests never fall through to standard
456
// service pathways, because that would let you use LFS tokens to read
457
// normal repository data.
458
if ($this->getIsGitLFSRequest()) {
459
return $this->serveGitLFSRequest($repository, $viewer);
460
}
461
462
// If this repository is hosted on a service, we need to proxy the request
463
// to a host which can serve it.
464
$is_cluster_request = $this->getRequest()->isProxiedClusterRequest();
465
466
$uri = $repository->getAlmanacServiceURI(
467
$viewer,
468
array(
469
'neverProxy' => $is_cluster_request,
470
'protocols' => array(
471
'http',
472
'https',
473
),
474
'writable' => !$this->isReadOnlyRequest($repository),
475
));
476
if ($uri) {
477
$future = $this->getRequest()->newClusterProxyFuture($uri);
478
return id(new AphrontHTTPProxyResponse())
479
->setHTTPFuture($future);
480
}
481
482
// Otherwise, we're going to handle the request locally.
483
484
$vcs_type = $repository->getVersionControlSystem();
485
switch ($vcs_type) {
486
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
487
$result = $this->serveGitRequest($repository, $viewer);
488
break;
489
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
490
$result = $this->serveMercurialRequest($repository, $viewer);
491
break;
492
}
493
494
return $result;
495
}
496
497
private function isReadOnlyRequest(
498
PhabricatorRepository $repository) {
499
$request = $this->getRequest();
500
$method = $_SERVER['REQUEST_METHOD'];
501
502
// TODO: This implementation is safe by default, but very incomplete.
503
504
if ($this->getIsGitLFSRequest()) {
505
return $this->isGitLFSReadOnlyRequest($repository);
506
}
507
508
switch ($repository->getVersionControlSystem()) {
509
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
510
$service = $request->getStr('service');
511
$path = $this->getRequestDirectoryPath($repository);
512
// NOTE: Service names are the reverse of what you might expect, as they
513
// are from the point of view of the server. The main read service is
514
// "git-upload-pack", and the main write service is "git-receive-pack".
515
516
if ($method == 'GET' &&
517
$path == '/info/refs' &&
518
$service == 'git-upload-pack') {
519
return true;
520
}
521
522
if ($path == '/git-upload-pack') {
523
return true;
524
}
525
526
break;
527
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
528
$cmd = $request->getStr('cmd');
529
if ($cmd === null) {
530
return false;
531
}
532
if ($cmd == 'batch') {
533
$cmds = idx($this->getMercurialArguments(), 'cmds');
534
if ($cmds !== null) {
535
return DiffusionMercurialWireProtocol::isReadOnlyBatchCommand(
536
$cmds);
537
}
538
}
539
return DiffusionMercurialWireProtocol::isReadOnlyCommand($cmd);
540
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
541
break;
542
}
543
544
return false;
545
}
546
547
/**
548
* @phutil-external-symbol class PhabricatorStartup
549
*/
550
private function serveGitRequest(
551
PhabricatorRepository $repository,
552
PhabricatorUser $viewer) {
553
$request = $this->getRequest();
554
555
$request_path = $this->getRequestDirectoryPath($repository);
556
$repository_root = $repository->getLocalPath();
557
558
// Rebuild the query string to strip `__magic__` parameters and prevent
559
// issues where we might interpret inputs like "service=read&service=write"
560
// differently than the server does and pass it an unsafe command.
561
562
// NOTE: This does not use getPassthroughRequestParameters() because
563
// that code is HTTP-method agnostic and will encode POST data.
564
565
$query_data = $_GET;
566
foreach ($query_data as $key => $value) {
567
if (!strncmp($key, '__', 2)) {
568
unset($query_data[$key]);
569
}
570
}
571
$query_string = phutil_build_http_querystring($query_data);
572
573
// We're about to wipe out PATH with the rest of the environment, so
574
// resolve the binary first.
575
$bin = Filesystem::resolveBinary('git-http-backend');
576
if (!$bin) {
577
throw new Exception(
578
pht(
579
'Unable to find `%s` in %s!',
580
'git-http-backend',
581
'$PATH'));
582
}
583
584
// NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already
585
// decompressed the request when we read the request body, so the body is
586
// just plain data with no encoding.
587
588
$env = array(
589
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
590
'QUERY_STRING' => $query_string,
591
'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'),
592
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'],
593
'GIT_PROJECT_ROOT' => $repository_root,
594
'GIT_HTTP_EXPORT_ALL' => '1',
595
'PATH_INFO' => $request_path,
596
597
'REMOTE_USER' => $viewer->getUsername(),
598
599
// TODO: Set these correctly.
600
// GIT_COMMITTER_NAME
601
// GIT_COMMITTER_EMAIL
602
) + $this->getCommonEnvironment($viewer);
603
604
$input = PhabricatorStartup::getRawInput();
605
606
$command = csprintf('%s', $bin);
607
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
608
609
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
610
611
$cluster_engine = id(new DiffusionRepositoryClusterEngine())
612
->setViewer($viewer)
613
->setRepository($repository);
614
615
$did_write_lock = false;
616
if ($this->isReadOnlyRequest($repository)) {
617
$cluster_engine->synchronizeWorkingCopyBeforeRead();
618
} else {
619
$did_write_lock = true;
620
$cluster_engine->synchronizeWorkingCopyBeforeWrite();
621
}
622
623
$caught = null;
624
try {
625
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
626
->setEnv($env, true)
627
->write($input)
628
->resolve();
629
} catch (Exception $ex) {
630
$caught = $ex;
631
}
632
633
if ($did_write_lock) {
634
$cluster_engine->synchronizeWorkingCopyAfterWrite();
635
}
636
637
unset($unguarded);
638
639
if ($caught) {
640
throw $caught;
641
}
642
643
if ($err) {
644
if ($this->isValidGitShallowCloneResponse($stdout, $stderr)) {
645
// Ignore the error if the response passes this special check for
646
// validity.
647
$err = 0;
648
}
649
}
650
651
if ($err) {
652
return new PhabricatorVCSResponse(
653
500,
654
pht(
655
'Error %d: %s',
656
$err,
657
phutil_utf8ize($stderr)));
658
}
659
660
return id(new DiffusionGitResponse())->setGitData($stdout);
661
}
662
663
private function getRequestDirectoryPath(PhabricatorRepository $repository) {
664
$request = $this->getRequest();
665
$request_path = $request->getRequestURI()->getPath();
666
667
$info = PhabricatorRepository::parseRepositoryServicePath(
668
$request_path,
669
$repository->getVersionControlSystem());
670
$base_path = $info['path'];
671
672
// For Git repositories, strip an optional directory component if it
673
// isn't the name of a known Git resource. This allows users to clone
674
// repositories as "/diffusion/X/anything.git", for example.
675
if ($repository->isGit()) {
676
$known = array(
677
'info',
678
'git-upload-pack',
679
'git-receive-pack',
680
);
681
682
foreach ($known as $key => $path) {
683
$known[$key] = preg_quote($path, '@');
684
}
685
686
$known = implode('|', $known);
687
688
if (preg_match('@^/([^/]+)/('.$known.')(/|$)@', $base_path)) {
689
$base_path = preg_replace('@^/([^/]+)@', '', $base_path);
690
}
691
}
692
693
return $base_path;
694
}
695
696
private function authenticateGitLFSUser(
697
$username,
698
PhutilOpaqueEnvelope $password,
699
$identifier) {
700
701
// Never accept these credentials for requests which aren't LFS requests.
702
if (!$this->getIsGitLFSRequest()) {
703
return null;
704
}
705
706
// If we have the wrong username, don't bother checking if the token
707
// is right.
708
if ($username !== DiffusionGitLFSTemporaryTokenType::HTTP_USERNAME) {
709
return null;
710
}
711
712
// See PHI1123. We need to be able to constrain the token query with
713
// "withTokenResources(...)" to take advantage of the key on the table.
714
// In this case, the repository PHID is the "resource" we're after.
715
716
// In normal workflows, we figure out the viewer first, then use the
717
// viewer to load the repository, but that won't work here. Load the
718
// repository as the omnipotent viewer, then use the repository PHID to
719
// look for a token.
720
721
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
722
723
$repository = id(new PhabricatorRepositoryQuery())
724
->setViewer($omnipotent_viewer)
725
->withIdentifiers(array($identifier))
726
->executeOne();
727
if (!$repository) {
728
return null;
729
}
730
731
$lfs_pass = $password->openEnvelope();
732
$lfs_hash = PhabricatorHash::weakDigest($lfs_pass);
733
734
$token = id(new PhabricatorAuthTemporaryTokenQuery())
735
->setViewer($omnipotent_viewer)
736
->withTokenResources(array($repository->getPHID()))
737
->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE))
738
->withTokenCodes(array($lfs_hash))
739
->withExpired(false)
740
->executeOne();
741
if (!$token) {
742
return null;
743
}
744
745
$user = id(new PhabricatorPeopleQuery())
746
->setViewer($omnipotent_viewer)
747
->withPHIDs(array($token->getUserPHID()))
748
->executeOne();
749
750
if (!$user) {
751
return null;
752
}
753
754
if (!$user->isUserActivated()) {
755
return null;
756
}
757
758
$this->gitLFSToken = $token;
759
760
return $user;
761
}
762
763
private function authenticateHTTPRepositoryUser(
764
$username,
765
PhutilOpaqueEnvelope $password) {
766
767
if (!PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth')) {
768
// No HTTP auth permitted.
769
return null;
770
}
771
772
if (!strlen($username)) {
773
// No username.
774
return null;
775
}
776
777
if (!strlen($password->openEnvelope())) {
778
// No password.
779
return null;
780
}
781
782
$user = id(new PhabricatorPeopleQuery())
783
->setViewer(PhabricatorUser::getOmnipotentUser())
784
->withUsernames(array($username))
785
->executeOne();
786
if (!$user) {
787
// Username doesn't match anything.
788
return null;
789
}
790
791
if (!$user->isUserActivated()) {
792
// User is not activated.
793
return null;
794
}
795
796
$request = $this->getRequest();
797
$content_source = PhabricatorContentSource::newFromRequest($request);
798
799
$engine = id(new PhabricatorAuthPasswordEngine())
800
->setViewer($user)
801
->setContentSource($content_source)
802
->setPasswordType(PhabricatorAuthPassword::PASSWORD_TYPE_VCS)
803
->setObject($user);
804
805
if (!$engine->isValidPassword($password)) {
806
return null;
807
}
808
809
return $user;
810
}
811
812
private function serveMercurialRequest(
813
PhabricatorRepository $repository,
814
PhabricatorUser $viewer) {
815
$request = $this->getRequest();
816
817
$bin = Filesystem::resolveBinary('hg');
818
if (!$bin) {
819
throw new Exception(
820
pht(
821
'Unable to find `%s` in %s!',
822
'hg',
823
'$PATH'));
824
}
825
826
$env = $this->getCommonEnvironment($viewer);
827
$input = PhabricatorStartup::getRawInput();
828
829
$cmd = $request->getStr('cmd');
830
831
$args = $this->getMercurialArguments();
832
$args = $this->formatMercurialArguments($cmd, $args);
833
834
if (strlen($input)) {
835
$input = strlen($input)."\n".$input."0\n";
836
}
837
838
$command = csprintf(
839
'%s -R %s serve --stdio',
840
$bin,
841
$repository->getLocalPath());
842
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
843
844
list($err, $stdout, $stderr) = id(new ExecFuture('%C', $command))
845
->setEnv($env, true)
846
->setCWD($repository->getLocalPath())
847
->write("{$cmd}\n{$args}{$input}")
848
->resolve();
849
850
if ($err) {
851
return new PhabricatorVCSResponse(
852
500,
853
pht('Error %d: %s', $err, $stderr));
854
}
855
856
if ($cmd == 'getbundle' ||
857
$cmd == 'changegroup' ||
858
$cmd == 'changegroupsubset') {
859
// We're not completely sure that "changegroup" and "changegroupsubset"
860
// actually work, they're for very old Mercurial.
861
$body = gzcompress($stdout);
862
} else if ($cmd == 'unbundle') {
863
// This includes diagnostic information and anything echoed by commit
864
// hooks. We ignore `stdout` since it just has protocol garbage, and
865
// substitute `stderr`.
866
$body = strlen($stderr)."\n".$stderr;
867
} else {
868
list($length, $body) = explode("\n", $stdout, 2);
869
if ($cmd == 'capabilities') {
870
$body = DiffusionMercurialWireProtocol::filterBundle2Capability($body);
871
}
872
}
873
874
return id(new DiffusionMercurialResponse())->setContent($body);
875
}
876
877
private function getMercurialArguments() {
878
// Mercurial sends arguments in HTTP headers. "Why?", you might wonder,
879
// "Why would you do this?".
880
881
$args_raw = array();
882
for ($ii = 1;; $ii++) {
883
$header = 'HTTP_X_HGARG_'.$ii;
884
if (!array_key_exists($header, $_SERVER)) {
885
break;
886
}
887
$args_raw[] = $_SERVER[$header];
888
}
889
890
if ($args_raw) {
891
$args_raw = implode('', $args_raw);
892
return id(new PhutilQueryStringParser())
893
->parseQueryString($args_raw);
894
}
895
896
// Sometimes arguments come in via the query string. Note that this will
897
// not handle multi-value entries e.g. "a[]=1,a[]=2" however it's unclear
898
// whether or how the mercurial protocol should handle this.
899
$query = idx($_SERVER, 'QUERY_STRING', '');
900
$query_pairs = id(new PhutilQueryStringParser())
901
->parseQueryString($query);
902
foreach ($query_pairs as $key => $value) {
903
// Filter out private/internal keys as well as the command itself.
904
if (strncmp($key, '__', 2) && $key != 'cmd') {
905
$args_raw[$key] = $value;
906
}
907
}
908
909
// TODO: Arguments can also come in via request body for POST requests. The
910
// body would be all arguments, url-encoded.
911
return $args_raw;
912
}
913
914
private function formatMercurialArguments($command, array $arguments) {
915
$spec = DiffusionMercurialWireProtocol::getCommandArgs($command);
916
917
$out = array();
918
919
// Mercurial takes normal arguments like this:
920
//
921
// name <length(value)>
922
// value
923
924
$has_star = false;
925
foreach ($spec as $arg_key) {
926
if ($arg_key == '*') {
927
$has_star = true;
928
continue;
929
}
930
if (isset($arguments[$arg_key])) {
931
$value = $arguments[$arg_key];
932
$size = strlen($value);
933
$out[] = "{$arg_key} {$size}\n{$value}";
934
unset($arguments[$arg_key]);
935
}
936
}
937
938
if ($has_star) {
939
940
// Mercurial takes arguments for variable argument lists roughly like
941
// this:
942
//
943
// * <count(args)>
944
// argname1 <length(argvalue1)>
945
// argvalue1
946
// argname2 <length(argvalue2)>
947
// argvalue2
948
949
$count = count($arguments);
950
951
$out[] = "* {$count}\n";
952
953
foreach ($arguments as $key => $value) {
954
if (in_array($key, $spec)) {
955
// We already added this argument above, so skip it.
956
continue;
957
}
958
$size = strlen($value);
959
$out[] = "{$key} {$size}\n{$value}";
960
}
961
}
962
963
return implode('', $out);
964
}
965
966
private function isValidGitShallowCloneResponse($stdout, $stderr) {
967
// If you execute `git clone --depth N ...`, git sends a request which
968
// `git-http-backend` responds to by emitting valid output and then exiting
969
// with a failure code and an error message. If we ignore this error,
970
// everything works.
971
972
// This is a pretty funky fix: it would be nice to more precisely detect
973
// that a request is a `--depth N` clone request, but we don't have any code
974
// to decode protocol frames yet. Instead, look for reasonable evidence
975
// in the output that we're looking at a `--depth` clone.
976
977
// A valid x-git-upload-pack-result response during packfile negotiation
978
// should end with a flush packet ("0000"). As long as that packet
979
// terminates the response body in the response, we'll assume the response
980
// is correct and complete.
981
982
// See https://git-scm.com/docs/pack-protocol#_packfile_negotiation
983
984
$stdout_regexp = '(^Content-Type: application/x-git-upload-pack-result)m';
985
986
$has_pack = preg_match($stdout_regexp, $stdout);
987
988
if (strlen($stdout) >= 4) {
989
$has_flush_packet = (substr($stdout, -4) === "0000");
990
} else {
991
$has_flush_packet = false;
992
}
993
994
return ($has_pack && $has_flush_packet);
995
}
996
997
private function getCommonEnvironment(PhabricatorUser $viewer) {
998
$remote_address = $this->getRequest()->getRemoteAddress();
999
1000
return array(
1001
DiffusionCommitHookEngine::ENV_USER => $viewer->getUsername(),
1002
DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS => $remote_address,
1003
DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'http',
1004
);
1005
}
1006
1007
private function validateGitLFSRequest(
1008
PhabricatorRepository $repository,
1009
PhabricatorUser $viewer) {
1010
if (!$this->getIsGitLFSRequest()) {
1011
return null;
1012
}
1013
1014
if (!$repository->canUseGitLFS()) {
1015
return new PhabricatorVCSResponse(
1016
403,
1017
pht(
1018
'The requested repository ("%s") does not support Git LFS.',
1019
$repository->getDisplayName()));
1020
}
1021
1022
// If this is using an LFS token, sanity check that we're using it on the
1023
// correct repository. This shouldn't really matter since the user could
1024
// just request a proper token anyway, but it suspicious and should not
1025
// be permitted.
1026
1027
$token = $this->getGitLFSToken();
1028
if ($token) {
1029
$resource = $token->getTokenResource();
1030
if ($resource !== $repository->getPHID()) {
1031
return new PhabricatorVCSResponse(
1032
403,
1033
pht(
1034
'The authentication token provided in the request is bound to '.
1035
'a different repository than the requested repository ("%s").',
1036
$repository->getDisplayName()));
1037
}
1038
}
1039
1040
return null;
1041
}
1042
1043
private function serveGitLFSRequest(
1044
PhabricatorRepository $repository,
1045
PhabricatorUser $viewer) {
1046
1047
if (!$this->getIsGitLFSRequest()) {
1048
throw new Exception(pht('This is not a Git LFS request!'));
1049
}
1050
1051
$path = $this->getGitLFSRequestPath($repository);
1052
$matches = null;
1053
1054
if (preg_match('(^upload/(.*)\z)', $path, $matches)) {
1055
$oid = $matches[1];
1056
return $this->serveGitLFSUploadRequest($repository, $viewer, $oid);
1057
} else if ($path == 'objects/batch') {
1058
return $this->serveGitLFSBatchRequest($repository, $viewer);
1059
} else {
1060
return DiffusionGitLFSResponse::newErrorResponse(
1061
404,
1062
pht(
1063
'Git LFS operation "%s" is not supported by this server.',
1064
$path));
1065
}
1066
}
1067
1068
private function serveGitLFSBatchRequest(
1069
PhabricatorRepository $repository,
1070
PhabricatorUser $viewer) {
1071
1072
$input = $this->getGitLFSInput();
1073
1074
$operation = idx($input, 'operation');
1075
switch ($operation) {
1076
case 'upload':
1077
$want_upload = true;
1078
break;
1079
case 'download':
1080
$want_upload = false;
1081
break;
1082
default:
1083
return DiffusionGitLFSResponse::newErrorResponse(
1084
404,
1085
pht(
1086
'Git LFS batch operation "%s" is not supported by this server.',
1087
$operation));
1088
}
1089
1090
$objects = idx($input, 'objects', array());
1091
1092
$hashes = array();
1093
foreach ($objects as $object) {
1094
$hashes[] = idx($object, 'oid');
1095
}
1096
1097
if ($hashes) {
1098
$refs = id(new PhabricatorRepositoryGitLFSRefQuery())
1099
->setViewer($viewer)
1100
->withRepositoryPHIDs(array($repository->getPHID()))
1101
->withObjectHashes($hashes)
1102
->execute();
1103
$refs = mpull($refs, null, 'getObjectHash');
1104
} else {
1105
$refs = array();
1106
}
1107
1108
$file_phids = mpull($refs, 'getFilePHID');
1109
if ($file_phids) {
1110
$files = id(new PhabricatorFileQuery())
1111
->setViewer($viewer)
1112
->withPHIDs($file_phids)
1113
->execute();
1114
$files = mpull($files, null, 'getPHID');
1115
} else {
1116
$files = array();
1117
}
1118
1119
$authorization = null;
1120
$output = array();
1121
foreach ($objects as $object) {
1122
$oid = idx($object, 'oid');
1123
$size = idx($object, 'size');
1124
$ref = idx($refs, $oid);
1125
$error = null;
1126
1127
// NOTE: If we already have a ref for this object, we only emit a
1128
// "download" action. The client should not upload the file again.
1129
1130
$actions = array();
1131
if ($ref) {
1132
$file = idx($files, $ref->getFilePHID());
1133
if ($file) {
1134
// Git LFS may prompt users for authentication if the action does
1135
// not provide an "Authorization" header and does not have a query
1136
// parameter named "token". See here for discussion:
1137
// <https://github.com/github/git-lfs/issues/1088>
1138
$no_authorization = 'Basic '.base64_encode('none');
1139
1140
$get_uri = $file->getCDNURI('data');
1141
$actions['download'] = array(
1142
'href' => $get_uri,
1143
'header' => array(
1144
'Authorization' => $no_authorization,
1145
'X-Phabricator-Request-Type' => 'git-lfs',
1146
),
1147
);
1148
} else {
1149
$error = array(
1150
'code' => 404,
1151
'message' => pht(
1152
'Object "%s" was previously uploaded, but no longer exists '.
1153
'on this server.',
1154
$oid),
1155
);
1156
}
1157
} else if ($want_upload) {
1158
if (!$authorization) {
1159
// Here, we could reuse the existing authorization if we have one,
1160
// but it's a little simpler to just generate a new one
1161
// unconditionally.
1162
$authorization = $this->newGitLFSHTTPAuthorization(
1163
$repository,
1164
$viewer,
1165
$operation);
1166
}
1167
1168
$put_uri = $repository->getGitLFSURI("info/lfs/upload/{$oid}");
1169
1170
$actions['upload'] = array(
1171
'href' => $put_uri,
1172
'header' => array(
1173
'Authorization' => $authorization,
1174
'X-Phabricator-Request-Type' => 'git-lfs',
1175
),
1176
);
1177
}
1178
1179
$object = array(
1180
'oid' => $oid,
1181
'size' => $size,
1182
);
1183
1184
if ($actions) {
1185
$object['actions'] = $actions;
1186
}
1187
1188
if ($error) {
1189
$object['error'] = $error;
1190
}
1191
1192
$output[] = $object;
1193
}
1194
1195
$output = array(
1196
'objects' => $output,
1197
);
1198
1199
return id(new DiffusionGitLFSResponse())
1200
->setContent($output);
1201
}
1202
1203
private function serveGitLFSUploadRequest(
1204
PhabricatorRepository $repository,
1205
PhabricatorUser $viewer,
1206
$oid) {
1207
1208
$ref = id(new PhabricatorRepositoryGitLFSRefQuery())
1209
->setViewer($viewer)
1210
->withRepositoryPHIDs(array($repository->getPHID()))
1211
->withObjectHashes(array($oid))
1212
->executeOne();
1213
if ($ref) {
1214
return DiffusionGitLFSResponse::newErrorResponse(
1215
405,
1216
pht(
1217
'Content for object "%s" is already known to this server. It can '.
1218
'not be uploaded again.',
1219
$oid));
1220
}
1221
1222
// Remove the execution time limit because uploading large files may take
1223
// a while.
1224
set_time_limit(0);
1225
1226
$request_stream = new AphrontRequestStream();
1227
$request_iterator = $request_stream->getIterator();
1228
$hashing_iterator = id(new PhutilHashingIterator($request_iterator))
1229
->setAlgorithm('sha256');
1230
1231
$source = id(new PhabricatorIteratorFileUploadSource())
1232
->setName('lfs-'.$oid)
1233
->setViewPolicy(PhabricatorPolicies::POLICY_NOONE)
1234
->setIterator($hashing_iterator);
1235
1236
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
1237
$file = $source->uploadFile();
1238
unset($unguarded);
1239
1240
$hash = $hashing_iterator->getHash();
1241
if ($hash !== $oid) {
1242
return DiffusionGitLFSResponse::newErrorResponse(
1243
400,
1244
pht(
1245
'Uploaded data is corrupt or invalid. Expected hash "%s", actual '.
1246
'hash "%s".',
1247
$oid,
1248
$hash));
1249
}
1250
1251
$ref = id(new PhabricatorRepositoryGitLFSRef())
1252
->setRepositoryPHID($repository->getPHID())
1253
->setObjectHash($hash)
1254
->setByteSize($file->getByteSize())
1255
->setAuthorPHID($viewer->getPHID())
1256
->setFilePHID($file->getPHID());
1257
1258
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
1259
// Attach the file to the repository to give users permission
1260
// to access it.
1261
$file->attachToObject($repository->getPHID());
1262
$ref->save();
1263
unset($unguarded);
1264
1265
// This is just a plain HTTP 200 with no content, which is what `git lfs`
1266
// expects.
1267
return new DiffusionGitLFSResponse();
1268
}
1269
1270
private function newGitLFSHTTPAuthorization(
1271
PhabricatorRepository $repository,
1272
PhabricatorUser $viewer,
1273
$operation) {
1274
1275
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
1276
1277
$authorization = DiffusionGitLFSTemporaryTokenType::newHTTPAuthorization(
1278
$repository,
1279
$viewer,
1280
$operation);
1281
1282
unset($unguarded);
1283
1284
return $authorization;
1285
}
1286
1287
private function getGitLFSRequestPath(PhabricatorRepository $repository) {
1288
$request_path = $this->getRequestDirectoryPath($repository);
1289
1290
$matches = null;
1291
if (preg_match('(^/info/lfs(?:\z|/)(.*))', $request_path, $matches)) {
1292
return $matches[1];
1293
}
1294
1295
return null;
1296
}
1297
1298
private function getGitLFSInput() {
1299
if (!$this->gitLFSInput) {
1300
$input = PhabricatorStartup::getRawInput();
1301
$input = phutil_json_decode($input);
1302
$this->gitLFSInput = $input;
1303
}
1304
1305
return $this->gitLFSInput;
1306
}
1307
1308
private function isGitLFSReadOnlyRequest(PhabricatorRepository $repository) {
1309
if (!$this->getIsGitLFSRequest()) {
1310
return false;
1311
}
1312
1313
$path = $this->getGitLFSRequestPath($repository);
1314
1315
if ($path === 'objects/batch') {
1316
$input = $this->getGitLFSInput();
1317
$operation = idx($input, 'operation');
1318
switch ($operation) {
1319
case 'download':
1320
return true;
1321
default:
1322
return false;
1323
}
1324
}
1325
1326
return false;
1327
}
1328
1329
1330
}
1331
1332