Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/aphront/AphrontRequest.php
12240 views
1
<?php
2
3
/**
4
* @task data Accessing Request Data
5
* @task cookie Managing Cookies
6
* @task cluster Working With a Phabricator Cluster
7
*/
8
final class AphrontRequest extends Phobject {
9
10
// NOTE: These magic request-type parameters are automatically included in
11
// certain requests (e.g., by phabricator_form(), JX.Request,
12
// JX.Workflow, and ConduitClient) and help us figure out what sort of
13
// response the client expects.
14
15
const TYPE_AJAX = '__ajax__';
16
const TYPE_FORM = '__form__';
17
const TYPE_CONDUIT = '__conduit__';
18
const TYPE_WORKFLOW = '__wflow__';
19
const TYPE_CONTINUE = '__continue__';
20
const TYPE_PREVIEW = '__preview__';
21
const TYPE_HISEC = '__hisec__';
22
const TYPE_QUICKSAND = '__quicksand__';
23
24
private $host;
25
private $path;
26
private $requestData;
27
private $user;
28
private $applicationConfiguration;
29
private $site;
30
private $controller;
31
private $uriData = array();
32
private $cookiePrefix;
33
private $submitKey;
34
35
public function __construct($host, $path) {
36
$this->host = $host;
37
$this->path = $path;
38
}
39
40
public function setURIMap(array $uri_data) {
41
$this->uriData = $uri_data;
42
return $this;
43
}
44
45
public function getURIMap() {
46
return $this->uriData;
47
}
48
49
public function getURIData($key, $default = null) {
50
return idx($this->uriData, $key, $default);
51
}
52
53
/**
54
* Read line range parameter data from the request.
55
*
56
* Applications like Paste, Diffusion, and Harbormaster use "$12-14" in the
57
* URI to allow users to link to particular lines.
58
*
59
* @param string URI data key to pull line range information from.
60
* @param int|null Maximum length of the range.
61
* @return null|pair<int, int> Null, or beginning and end of the range.
62
*/
63
public function getURILineRange($key, $limit) {
64
$range = $this->getURIData($key);
65
return self::parseURILineRange($range, $limit);
66
}
67
68
public static function parseURILineRange($range, $limit) {
69
if ($range === null || !strlen($range)) {
70
return null;
71
}
72
73
$range = explode('-', $range, 2);
74
75
foreach ($range as $key => $value) {
76
$value = (int)$value;
77
if (!$value) {
78
// If either value is "0", discard the range.
79
return null;
80
}
81
$range[$key] = $value;
82
}
83
84
// If the range is like "$10", treat it like "$10-10".
85
if (count($range) == 1) {
86
$range[] = head($range);
87
}
88
89
// If the range is "$7-5", treat it like "$5-7".
90
if ($range[1] < $range[0]) {
91
$range = array_reverse($range);
92
}
93
94
// If the user specified something like "$1-999999999" and we have a limit,
95
// clamp it to a more reasonable range.
96
if ($limit !== null) {
97
if ($range[1] - $range[0] > $limit) {
98
$range[1] = $range[0] + $limit;
99
}
100
}
101
102
return $range;
103
}
104
105
public function setApplicationConfiguration(
106
$application_configuration) {
107
$this->applicationConfiguration = $application_configuration;
108
return $this;
109
}
110
111
public function getApplicationConfiguration() {
112
return $this->applicationConfiguration;
113
}
114
115
public function setPath($path) {
116
$this->path = $path;
117
return $this;
118
}
119
120
public function getPath() {
121
return $this->path;
122
}
123
124
public function getHost() {
125
// The "Host" header may include a port number, or may be a malicious
126
// header in the form "realdomain.com:[email protected]". Invoke the full
127
// parser to extract the real domain correctly. See here for coverage of
128
// a similar issue in Django:
129
//
130
// https://www.djangoproject.com/weblog/2012/oct/17/security/
131
$uri = new PhutilURI('http://'.$this->host);
132
return $uri->getDomain();
133
}
134
135
public function setSite(AphrontSite $site) {
136
$this->site = $site;
137
return $this;
138
}
139
140
public function getSite() {
141
return $this->site;
142
}
143
144
public function setController(AphrontController $controller) {
145
$this->controller = $controller;
146
return $this;
147
}
148
149
public function getController() {
150
return $this->controller;
151
}
152
153
154
/* -( Accessing Request Data )--------------------------------------------- */
155
156
157
/**
158
* @task data
159
*/
160
public function setRequestData(array $request_data) {
161
$this->requestData = $request_data;
162
return $this;
163
}
164
165
166
/**
167
* @task data
168
*/
169
public function getRequestData() {
170
return $this->requestData;
171
}
172
173
174
/**
175
* @task data
176
*/
177
public function getInt($name, $default = null) {
178
if (isset($this->requestData[$name])) {
179
// Converting from array to int is "undefined". Don't rely on whatever
180
// PHP decides to do.
181
if (is_array($this->requestData[$name])) {
182
return $default;
183
}
184
return (int)$this->requestData[$name];
185
} else {
186
return $default;
187
}
188
}
189
190
191
/**
192
* @task data
193
*/
194
public function getBool($name, $default = null) {
195
if (isset($this->requestData[$name])) {
196
if ($this->requestData[$name] === 'true') {
197
return true;
198
} else if ($this->requestData[$name] === 'false') {
199
return false;
200
} else {
201
return (bool)$this->requestData[$name];
202
}
203
} else {
204
return $default;
205
}
206
}
207
208
209
/**
210
* @task data
211
*/
212
public function getStr($name, $default = null) {
213
if (isset($this->requestData[$name])) {
214
$str = (string)$this->requestData[$name];
215
// Normalize newline craziness.
216
$str = str_replace(
217
array("\r\n", "\r"),
218
array("\n", "\n"),
219
$str);
220
return $str;
221
} else {
222
return $default;
223
}
224
}
225
226
227
/**
228
* @task data
229
*/
230
public function getJSONMap($name, $default = array()) {
231
if (!isset($this->requestData[$name])) {
232
return $default;
233
}
234
235
$raw_data = phutil_string_cast($this->requestData[$name]);
236
$raw_data = trim($raw_data);
237
if (!strlen($raw_data)) {
238
return $default;
239
}
240
241
if ($raw_data[0] !== '{') {
242
throw new Exception(
243
pht(
244
'Request parameter "%s" is not formatted properly. Expected a '.
245
'JSON object, but value does not start with "{".',
246
$name));
247
}
248
249
try {
250
$json_object = phutil_json_decode($raw_data);
251
} catch (PhutilJSONParserException $ex) {
252
throw new Exception(
253
pht(
254
'Request parameter "%s" is not formatted properly. Expected a '.
255
'JSON object, but encountered a syntax error: %s.',
256
$name,
257
$ex->getMessage()));
258
}
259
260
return $json_object;
261
}
262
263
264
/**
265
* @task data
266
*/
267
public function getArr($name, $default = array()) {
268
if (isset($this->requestData[$name]) &&
269
is_array($this->requestData[$name])) {
270
return $this->requestData[$name];
271
} else {
272
return $default;
273
}
274
}
275
276
277
/**
278
* @task data
279
*/
280
public function getStrList($name, $default = array()) {
281
if (!isset($this->requestData[$name])) {
282
return $default;
283
}
284
$list = $this->getStr($name);
285
$list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
286
return $list;
287
}
288
289
290
/**
291
* @task data
292
*/
293
public function getExists($name) {
294
return array_key_exists($name, $this->requestData);
295
}
296
297
public function getFileExists($name) {
298
return isset($_FILES[$name]) &&
299
(idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
300
}
301
302
public function isHTTPGet() {
303
return ($_SERVER['REQUEST_METHOD'] == 'GET');
304
}
305
306
public function isHTTPPost() {
307
return ($_SERVER['REQUEST_METHOD'] == 'POST');
308
}
309
310
public function isAjax() {
311
return $this->getExists(self::TYPE_AJAX) && !$this->isQuicksand();
312
}
313
314
public function isWorkflow() {
315
return $this->getExists(self::TYPE_WORKFLOW) && !$this->isQuicksand();
316
}
317
318
public function isQuicksand() {
319
return $this->getExists(self::TYPE_QUICKSAND);
320
}
321
322
public function isConduit() {
323
return $this->getExists(self::TYPE_CONDUIT);
324
}
325
326
public static function getCSRFTokenName() {
327
return '__csrf__';
328
}
329
330
public static function getCSRFHeaderName() {
331
return 'X-Phabricator-Csrf';
332
}
333
334
public static function getViaHeaderName() {
335
return 'X-Phabricator-Via';
336
}
337
338
public function validateCSRF() {
339
$token_name = self::getCSRFTokenName();
340
$token = $this->getStr($token_name);
341
342
// No token in the request, check the HTTP header which is added for Ajax
343
// requests.
344
if (empty($token)) {
345
$token = self::getHTTPHeader(self::getCSRFHeaderName());
346
}
347
348
$valid = $this->getUser()->validateCSRFToken($token);
349
if (!$valid) {
350
351
// Add some diagnostic details so we can figure out if some CSRF issues
352
// are JS problems or people accessing Ajax URIs directly with their
353
// browsers.
354
$info = array();
355
356
$info[] = pht(
357
'You are trying to save some data to permanent storage, but the '.
358
'request your browser made included an incorrect token. Reload the '.
359
'page and try again. You may need to clear your cookies.');
360
361
if ($this->isAjax()) {
362
$info[] = pht('This was an Ajax request.');
363
} else {
364
$info[] = pht('This was a Web request.');
365
}
366
367
if ($token) {
368
$info[] = pht('This request had an invalid CSRF token.');
369
} else {
370
$info[] = pht('This request had no CSRF token.');
371
}
372
373
// Give a more detailed explanation of how to avoid the exception
374
// in developer mode.
375
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
376
// TODO: Clean this up, see T1921.
377
$info[] = pht(
378
"To avoid this error, use %s to construct forms. If you are already ".
379
"using %s, make sure the form 'action' uses a relative URI (i.e., ".
380
"begins with a '%s'). Forms using absolute URIs do not include CSRF ".
381
"tokens, to prevent leaking tokens to external sites.\n\n".
382
"If this page performs writes which do not require CSRF protection ".
383
"(usually, filling caches or logging), you can use %s to ".
384
"temporarily bypass CSRF protection while writing. You should use ".
385
"this only for writes which can not be protected with normal CSRF ".
386
"mechanisms.\n\n".
387
"Some UI elements (like %s) also have methods which will allow you ".
388
"to render links as forms (like %s).",
389
'phabricator_form()',
390
'phabricator_form()',
391
'/',
392
'AphrontWriteGuard::beginScopedUnguardedWrites()',
393
'PhabricatorActionListView',
394
'setRenderAsForm(true)');
395
}
396
397
$message = implode("\n", $info);
398
399
// This should only be able to happen if you load a form, pull your
400
// internet for 6 hours, and then reconnect and immediately submit,
401
// but give the user some indication of what happened since the workflow
402
// is incredibly confusing otherwise.
403
throw new AphrontMalformedRequestException(
404
pht('Invalid Request (CSRF)'),
405
$message,
406
true);
407
}
408
409
return true;
410
}
411
412
public function isFormPost() {
413
$post = $this->getExists(self::TYPE_FORM) &&
414
!$this->getExists(self::TYPE_HISEC) &&
415
$this->isHTTPPost();
416
417
if (!$post) {
418
return false;
419
}
420
421
return $this->validateCSRF();
422
}
423
424
public function hasCSRF() {
425
try {
426
$this->validateCSRF();
427
return true;
428
} catch (AphrontMalformedRequestException $ex) {
429
return false;
430
}
431
}
432
433
public function isFormOrHisecPost() {
434
$post = $this->getExists(self::TYPE_FORM) &&
435
$this->isHTTPPost();
436
437
if (!$post) {
438
return false;
439
}
440
441
return $this->validateCSRF();
442
}
443
444
445
public function setCookiePrefix($prefix) {
446
$this->cookiePrefix = $prefix;
447
return $this;
448
}
449
450
private function getPrefixedCookieName($name) {
451
if ($this->cookiePrefix !== null && strlen($this->cookiePrefix)) {
452
return $this->cookiePrefix.'_'.$name;
453
}
454
return $name;
455
}
456
457
public function getCookie($name, $default = null) {
458
$name = $this->getPrefixedCookieName($name);
459
$value = idx($_COOKIE, $name, $default);
460
461
// Internally, PHP deletes cookies by setting them to the value 'deleted'
462
// with an expiration date in the past.
463
464
// At least in Safari, the browser may send this cookie anyway in some
465
// circumstances. After logging out, the 302'd GET to /login/ consistently
466
// includes deleted cookies on my local install. If a cookie value is
467
// literally 'deleted', pretend it does not exist.
468
469
if ($value === 'deleted') {
470
return null;
471
}
472
473
return $value;
474
}
475
476
public function clearCookie($name) {
477
$this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
478
unset($_COOKIE[$name]);
479
}
480
481
/**
482
* Get the domain which cookies should be set on for this request, or null
483
* if the request does not correspond to a valid cookie domain.
484
*
485
* @return PhutilURI|null Domain URI, or null if no valid domain exists.
486
*
487
* @task cookie
488
*/
489
private function getCookieDomainURI() {
490
if (PhabricatorEnv::getEnvConfig('security.require-https') &&
491
!$this->isHTTPS()) {
492
return null;
493
}
494
495
$host = $this->getHost();
496
497
// If there's no base domain configured, just use whatever the request
498
// domain is. This makes setup easier, and we'll tell administrators to
499
// configure a base domain during the setup process.
500
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
501
if ($base_uri === null || !strlen($base_uri)) {
502
return new PhutilURI('http://'.$host.'/');
503
}
504
505
$alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
506
$allowed_uris = array_merge(
507
array($base_uri),
508
$alternates);
509
510
foreach ($allowed_uris as $allowed_uri) {
511
$uri = new PhutilURI($allowed_uri);
512
if ($uri->getDomain() == $host) {
513
return $uri;
514
}
515
}
516
517
return null;
518
}
519
520
/**
521
* Determine if security policy rules will allow cookies to be set when
522
* responding to the request.
523
*
524
* @return bool True if setCookie() will succeed. If this method returns
525
* false, setCookie() will throw.
526
*
527
* @task cookie
528
*/
529
public function canSetCookies() {
530
return (bool)$this->getCookieDomainURI();
531
}
532
533
534
/**
535
* Set a cookie which does not expire for a long time.
536
*
537
* To set a temporary cookie, see @{method:setTemporaryCookie}.
538
*
539
* @param string Cookie name.
540
* @param string Cookie value.
541
* @return this
542
* @task cookie
543
*/
544
public function setCookie($name, $value) {
545
$far_future = time() + (60 * 60 * 24 * 365 * 5);
546
return $this->setCookieWithExpiration($name, $value, $far_future);
547
}
548
549
550
/**
551
* Set a cookie which expires soon.
552
*
553
* To set a durable cookie, see @{method:setCookie}.
554
*
555
* @param string Cookie name.
556
* @param string Cookie value.
557
* @return this
558
* @task cookie
559
*/
560
public function setTemporaryCookie($name, $value) {
561
return $this->setCookieWithExpiration($name, $value, 0);
562
}
563
564
565
/**
566
* Set a cookie with a given expiration policy.
567
*
568
* @param string Cookie name.
569
* @param string Cookie value.
570
* @param int Epoch timestamp for cookie expiration.
571
* @return this
572
* @task cookie
573
*/
574
private function setCookieWithExpiration(
575
$name,
576
$value,
577
$expire) {
578
579
$is_secure = false;
580
581
$base_domain_uri = $this->getCookieDomainURI();
582
if (!$base_domain_uri) {
583
$configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
584
$accessed_as = $this->getHost();
585
586
throw new AphrontMalformedRequestException(
587
pht('Bad Host Header'),
588
pht(
589
'This server is configured as "%s", but you are using the domain '.
590
'name "%s" to access a page which is trying to set a cookie. '.
591
'Access this service on the configured primary domain or a '.
592
'configured alternate domain. Cookies will not be set on other '.
593
'domains for security reasons.',
594
$configured_as,
595
$accessed_as),
596
true);
597
}
598
599
$base_domain = $base_domain_uri->getDomain();
600
$is_secure = ($base_domain_uri->getProtocol() == 'https');
601
602
$name = $this->getPrefixedCookieName($name);
603
604
if (php_sapi_name() == 'cli') {
605
// Do nothing, to avoid triggering "Cannot modify header information"
606
// warnings.
607
608
// TODO: This is effectively a test for whether we're running in a unit
609
// test or not. Move this actual call to HTTPSink?
610
} else {
611
setcookie(
612
$name,
613
$value,
614
$expire,
615
$path = '/',
616
$base_domain,
617
$is_secure,
618
$http_only = true);
619
}
620
621
$_COOKIE[$name] = $value;
622
623
return $this;
624
}
625
626
public function setUser($user) {
627
$this->user = $user;
628
return $this;
629
}
630
631
public function getUser() {
632
return $this->user;
633
}
634
635
public function getViewer() {
636
return $this->user;
637
}
638
639
public function getRequestURI() {
640
$uri_path = phutil_escape_uri($this->getPath());
641
$uri_query = idx($_SERVER, 'QUERY_STRING', '');
642
643
return id(new PhutilURI($uri_path.'?'.$uri_query))
644
->removeQueryParam('__path__');
645
}
646
647
public function getAbsoluteRequestURI() {
648
$uri = $this->getRequestURI();
649
$uri->setDomain($this->getHost());
650
651
if ($this->isHTTPS()) {
652
$protocol = 'https';
653
} else {
654
$protocol = 'http';
655
}
656
657
$uri->setProtocol($protocol);
658
659
// If the request used a nonstandard port, preserve it while building the
660
// absolute URI.
661
662
// First, get the default port for the request protocol.
663
$default_port = id(new PhutilURI($protocol.'://example.com/'))
664
->getPortWithProtocolDefault();
665
666
// NOTE: See note in getHost() about malicious "Host" headers. This
667
// construction defuses some obscure potential attacks.
668
$port = id(new PhutilURI($protocol.'://'.$this->host))
669
->getPort();
670
671
if (($port !== null) && ($port !== $default_port)) {
672
$uri->setPort($port);
673
}
674
675
return $uri;
676
}
677
678
public function isDialogFormPost() {
679
return $this->isFormPost() && $this->getStr('__dialog__');
680
}
681
682
public function getRemoteAddress() {
683
$address = PhabricatorEnv::getRemoteAddress();
684
685
if (!$address) {
686
return null;
687
}
688
689
return $address->getAddress();
690
}
691
692
public function isHTTPS() {
693
if (empty($_SERVER['HTTPS'])) {
694
return false;
695
}
696
if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
697
return false;
698
}
699
return true;
700
}
701
702
public function isContinueRequest() {
703
return $this->isFormOrHisecPost() && $this->getStr('__continue__');
704
}
705
706
public function isPreviewRequest() {
707
return $this->isFormPost() && $this->getStr('__preview__');
708
}
709
710
/**
711
* Get application request parameters in a flattened form suitable for
712
* inclusion in an HTTP request, excluding parameters with special meanings.
713
* This is primarily useful if you want to ask the user for more input and
714
* then resubmit their request.
715
*
716
* @return dict<string, string> Original request parameters.
717
*/
718
public function getPassthroughRequestParameters($include_quicksand = false) {
719
return self::flattenData(
720
$this->getPassthroughRequestData($include_quicksand));
721
}
722
723
/**
724
* Get request data other than "magic" parameters.
725
*
726
* @return dict<string, wild> Request data, with magic filtered out.
727
*/
728
public function getPassthroughRequestData($include_quicksand = false) {
729
$data = $this->getRequestData();
730
731
// Remove magic parameters like __dialog__ and __ajax__.
732
foreach ($data as $key => $value) {
733
if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
734
continue;
735
}
736
if (!strncmp($key, '__', 2)) {
737
unset($data[$key]);
738
}
739
}
740
741
return $data;
742
}
743
744
745
/**
746
* Flatten an array of key-value pairs (possibly including arrays as values)
747
* into a list of key-value pairs suitable for submitting via HTTP request
748
* (with arrays flattened).
749
*
750
* @param dict<string, wild> Data to flatten.
751
* @return dict<string, string> Flat data suitable for inclusion in an HTTP
752
* request.
753
*/
754
public static function flattenData(array $data) {
755
$result = array();
756
foreach ($data as $key => $value) {
757
if (is_array($value)) {
758
foreach (self::flattenData($value) as $fkey => $fvalue) {
759
$fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
760
$result[$key.$fkey] = $fvalue;
761
}
762
} else {
763
$result[$key] = (string)$value;
764
}
765
}
766
767
ksort($result);
768
769
return $result;
770
}
771
772
773
/**
774
* Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
775
*
776
* This function accepts a canonical header name, like `"Accept-Encoding"`,
777
* and looks up the appropriate value in `$_SERVER` (in this case,
778
* `"HTTP_ACCEPT_ENCODING"`).
779
*
780
* @param string Canonical header name, like `"Accept-Encoding"`.
781
* @param wild Default value to return if header is not present.
782
* @param array? Read this instead of `$_SERVER`.
783
* @return string|wild Header value if present, or `$default` if not.
784
*/
785
public static function getHTTPHeader($name, $default = null, $data = null) {
786
// PHP mangles HTTP headers by uppercasing them and replacing hyphens with
787
// underscores, then prepending 'HTTP_'.
788
$php_index = strtoupper($name);
789
$php_index = str_replace('-', '_', $php_index);
790
791
$try_names = array();
792
793
$try_names[] = 'HTTP_'.$php_index;
794
if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
795
// These headers may be available under alternate names. See
796
// http://www.php.net/manual/en/reserved.variables.server.php#110763
797
$try_names[] = $php_index;
798
}
799
800
if ($data === null) {
801
$data = $_SERVER;
802
}
803
804
foreach ($try_names as $try_name) {
805
if (array_key_exists($try_name, $data)) {
806
return $data[$try_name];
807
}
808
}
809
810
return $default;
811
}
812
813
814
/* -( Working With a Phabricator Cluster )--------------------------------- */
815
816
817
/**
818
* Is this a proxied request originating from within the Phabricator cluster?
819
*
820
* IMPORTANT: This means the request is dangerous!
821
*
822
* These requests are **more dangerous** than normal requests (they can not
823
* be safely proxied, because proxying them may cause a loop). Cluster
824
* requests are not guaranteed to come from a trusted source, and should
825
* never be treated as safer than normal requests. They are strictly less
826
* safe.
827
*/
828
public function isProxiedClusterRequest() {
829
return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
830
}
831
832
833
/**
834
* Build a new @{class:HTTPSFuture} which proxies this request to another
835
* node in the cluster.
836
*
837
* IMPORTANT: This is very dangerous!
838
*
839
* The future forwards authentication information present in the request.
840
* Proxied requests must only be sent to trusted hosts. (We attempt to
841
* enforce this.)
842
*
843
* This is not a general-purpose proxying method; it is a specialized
844
* method with niche applications and severe security implications.
845
*
846
* @param string URI identifying the host we are proxying the request to.
847
* @return HTTPSFuture New proxy future.
848
*
849
* @phutil-external-symbol class PhabricatorStartup
850
*/
851
public function newClusterProxyFuture($uri) {
852
$uri = new PhutilURI($uri);
853
854
$domain = $uri->getDomain();
855
$ip = gethostbyname($domain);
856
if (!$ip) {
857
throw new Exception(
858
pht(
859
'Unable to resolve domain "%s"!',
860
$domain));
861
}
862
863
if (!PhabricatorEnv::isClusterAddress($ip)) {
864
throw new Exception(
865
pht(
866
'Refusing to proxy a request to IP address ("%s") which is not '.
867
'in the cluster address block (this address was derived by '.
868
'resolving the domain "%s").',
869
$ip,
870
$domain));
871
}
872
873
$uri->setPath($this->getPath());
874
$uri->removeAllQueryParams();
875
foreach (self::flattenData($_GET) as $query_key => $query_value) {
876
$uri->appendQueryParam($query_key, $query_value);
877
}
878
879
$input = PhabricatorStartup::getRawInput();
880
881
$future = id(new HTTPSFuture($uri))
882
->addHeader('Host', self::getHost())
883
->addHeader('X-Phabricator-Cluster', true)
884
->setMethod($_SERVER['REQUEST_METHOD'])
885
->write($input);
886
887
if (isset($_SERVER['PHP_AUTH_USER'])) {
888
$future->setHTTPBasicAuthCredentials(
889
$_SERVER['PHP_AUTH_USER'],
890
new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
891
}
892
893
$headers = array();
894
$seen = array();
895
896
// NOTE: apache_request_headers() might provide a nicer way to do this,
897
// but isn't available under FCGI until PHP 5.4.0.
898
foreach ($_SERVER as $key => $value) {
899
if (!preg_match('/^HTTP_/', $key)) {
900
continue;
901
}
902
903
// Unmangle the header as best we can.
904
$key = substr($key, strlen('HTTP_'));
905
$key = str_replace('_', ' ', $key);
906
$key = strtolower($key);
907
$key = ucwords($key);
908
$key = str_replace(' ', '-', $key);
909
910
// By default, do not forward headers.
911
$should_forward = false;
912
913
// Forward "X-Hgarg-..." headers.
914
if (preg_match('/^X-Hgarg-/', $key)) {
915
$should_forward = true;
916
}
917
918
if ($should_forward) {
919
$headers[] = array($key, $value);
920
$seen[$key] = true;
921
}
922
}
923
924
// In some situations, this may not be mapped into the HTTP_X constants.
925
// CONTENT_LENGTH is similarly affected, but we trust cURL to take care
926
// of that if it matters, since we're handing off a request body.
927
if (empty($seen['Content-Type'])) {
928
if (isset($_SERVER['CONTENT_TYPE'])) {
929
$headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
930
}
931
}
932
933
foreach ($headers as $header) {
934
list($key, $value) = $header;
935
switch ($key) {
936
case 'Host':
937
case 'Authorization':
938
// Don't forward these headers, we've already handled them elsewhere.
939
unset($headers[$key]);
940
break;
941
default:
942
break;
943
}
944
}
945
946
foreach ($headers as $header) {
947
list($key, $value) = $header;
948
$future->addHeader($key, $value);
949
}
950
951
return $future;
952
}
953
954
public function updateEphemeralCookies() {
955
$submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
956
957
$submit_key = $this->getCookie($submit_cookie);
958
if ($submit_key !== null && strlen($submit_key)) {
959
$this->clearCookie($submit_cookie);
960
$this->submitKey = $submit_key;
961
}
962
963
}
964
965
public function getSubmitKey() {
966
return $this->submitKey;
967
}
968
969
}
970
971