@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<?php
2
3/**
4 * @task data Accessing Request Data
5 * @task cookie Managing Cookies
6 * @task cluster Working With a Phabricator Cluster
7 */
8final 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 $key URI data key to pull line range information from.
60 * @param int|null $limit Maximum length of the range.
61 * @return null|array<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 (!phutil_nonempty_string($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:ignored@evil.com". 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 = phutil_string_cast($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 (!phutil_nonempty_string($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 (phutil_nonempty_string($this->cookiePrefix)) {
452 return $this->cookiePrefix.'_'.$name;
453 } else {
454 return $name;
455 }
456 }
457
458 public function getCookie($name, $default = null) {
459 $name = $this->getPrefixedCookieName($name);
460 $value = idx($_COOKIE, $name, $default);
461
462 // Internally, PHP deletes cookies by setting them to the value 'deleted'
463 // with an expiration date in the past.
464
465 // At least in Safari, the browser may send this cookie anyway in some
466 // circumstances. After logging out, the 302'd GET to /login/ consistently
467 // includes deleted cookies on my local install. If a cookie value is
468 // literally 'deleted', pretend it does not exist.
469
470 if ($value === 'deleted') {
471 return null;
472 }
473
474 return $value;
475 }
476
477 public function clearCookie($name) {
478 $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
479 unset($_COOKIE[$name]);
480 }
481
482 /**
483 * Get the domain which cookies should be set on for this request, or null
484 * if the request does not correspond to a valid cookie domain.
485 *
486 * @return PhutilURI|null Domain URI, or null if no valid domain exists.
487 *
488 * @task cookie
489 */
490 private function getCookieDomainURI() {
491 if (PhabricatorEnv::getEnvConfig('security.require-https') &&
492 !$this->isHTTPS()) {
493 return null;
494 }
495
496 $host = $this->getHost();
497
498 // If there's no base domain configured, just use whatever the request
499 // domain is. This makes setup easier, and we'll tell administrators to
500 // configure a base domain during the setup process.
501 $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
502 if (!phutil_nonempty_string($base_uri)) {
503 return new PhutilURI('http://'.$host.'/');
504 }
505
506 $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
507 $allowed_uris = array_merge(
508 array($base_uri),
509 $alternates);
510
511 foreach ($allowed_uris as $allowed_uri) {
512 $uri = new PhutilURI($allowed_uri);
513 if ($uri->getDomain() == $host) {
514 return $uri;
515 }
516 }
517
518 return null;
519 }
520
521 /**
522 * Determine if security policy rules will allow cookies to be set when
523 * responding to the request.
524 *
525 * @return bool True if setCookie() will succeed. If this method returns
526 * false, setCookie() will throw.
527 *
528 * @task cookie
529 */
530 public function canSetCookies() {
531 return (bool)$this->getCookieDomainURI();
532 }
533
534
535 /**
536 * Set a cookie which does not expire for a long time.
537 *
538 * To set a temporary cookie, see @{method:setTemporaryCookie}.
539 *
540 * @param string $name Cookie name.
541 * @param string $value Cookie value.
542 * @return $this
543 * @task cookie
544 */
545 public function setCookie($name, $value) {
546 $far_future = time() + (60 * 60 * 24 * 365 * 5);
547 return $this->setCookieWithExpiration($name, $value, $far_future);
548 }
549
550
551 /**
552 * Set a cookie which expires soon.
553 *
554 * To set a durable cookie, see @{method:setCookie}.
555 *
556 * @param string $name Cookie name.
557 * @param string $value Cookie value.
558 * @return $this
559 * @task cookie
560 */
561 public function setTemporaryCookie($name, $value) {
562 return $this->setCookieWithExpiration($name, $value, 0);
563 }
564
565
566 /**
567 * Set a cookie with a given expiration policy.
568 *
569 * @param string $name Cookie name.
570 * @param string $value Cookie value.
571 * @param int $expire Epoch timestamp for cookie expiration.
572 * @return $this
573 * @task cookie
574 */
575 private function setCookieWithExpiration(
576 $name,
577 $value,
578 $expire) {
579
580 $is_secure = false;
581
582 $base_domain_uri = $this->getCookieDomainURI();
583 if (!$base_domain_uri) {
584 $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
585 $accessed_as = $this->getHost();
586
587 throw new AphrontMalformedRequestException(
588 pht('Bad Host Header'),
589 pht(
590 'This server is configured as "%s", but you are using the domain '.
591 'name "%s" to access a page which is trying to set a cookie. '.
592 'Access this service on the configured primary domain or a '.
593 'configured alternate domain. Cookies will not be set on other '.
594 'domains for security reasons.',
595 $configured_as,
596 $accessed_as),
597 true);
598 }
599
600 $base_domain = $base_domain_uri->getDomain();
601 $is_secure = ($base_domain_uri->getProtocol() == 'https');
602
603 $name = $this->getPrefixedCookieName($name);
604
605 if (php_sapi_name() == 'cli') {
606 // Do nothing, to avoid triggering "Cannot modify header information"
607 // warnings.
608
609 // TODO: This is effectively a test for whether we're running in a unit
610 // test or not. Move this actual call to HTTPSink?
611 } else {
612 setcookie(
613 $name,
614 $value,
615 $expire,
616 $path = '/',
617 $base_domain,
618 $is_secure,
619 $http_only = true);
620 }
621
622 $_COOKIE[$name] = $value;
623
624 return $this;
625 }
626
627 public function setUser($user) {
628 $this->user = $user;
629 return $this;
630 }
631
632 public function getUser() {
633 return $this->user;
634 }
635
636 public function getViewer() {
637 return $this->user;
638 }
639
640 public function getRequestURI() {
641 $uri_path = phutil_escape_uri($this->getPath());
642 $uri_query = idx($_SERVER, 'QUERY_STRING', '');
643
644 return id(new PhutilURI($uri_path.'?'.$uri_query))
645 ->removeQueryParam('__path__');
646 }
647
648 public function getAbsoluteRequestURI() {
649 $uri = $this->getRequestURI();
650 $uri->setDomain($this->getHost());
651
652 if ($this->isHTTPS()) {
653 $protocol = 'https';
654 } else {
655 $protocol = 'http';
656 }
657
658 $uri->setProtocol($protocol);
659
660 // If the request used a nonstandard port, preserve it while building the
661 // absolute URI.
662
663 // First, get the default port for the request protocol.
664 $default_port = id(new PhutilURI($protocol.'://example.com/'))
665 ->getPortWithProtocolDefault();
666
667 // NOTE: See note in getHost() about malicious "Host" headers. This
668 // construction defuses some obscure potential attacks.
669 $port = id(new PhutilURI($protocol.'://'.$this->host))
670 ->getPort();
671
672 if (($port !== null) && ($port !== $default_port)) {
673 $uri->setPort($port);
674 }
675
676 return $uri;
677 }
678
679 public function isDialogFormPost() {
680 return $this->isFormPost() && $this->getStr('__dialog__');
681 }
682
683 public function getRemoteAddress() {
684 $address = PhabricatorEnv::getRemoteAddress();
685
686 if (!$address) {
687 return null;
688 }
689
690 return $address->getAddress();
691 }
692
693 public function isHTTPS() {
694 if (empty($_SERVER['HTTPS'])) {
695 return false;
696 }
697 if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
698 return false;
699 }
700 return true;
701 }
702
703 public function isContinueRequest() {
704 return $this->isFormOrHisecPost() && $this->getStr('__continue__');
705 }
706
707 public function isPreviewRequest() {
708 return $this->isFormPost() && $this->getStr('__preview__');
709 }
710
711 /**
712 * Get application request parameters in a flattened form suitable for
713 * inclusion in an HTTP request, excluding parameters with special meanings.
714 * This is primarily useful if you want to ask the user for more input and
715 * then resubmit their request.
716 *
717 * @return array<string, string> Original request parameters.
718 */
719 public function getPassthroughRequestParameters($include_quicksand = false) {
720 return self::flattenData(
721 $this->getPassthroughRequestData($include_quicksand));
722 }
723
724 /**
725 * Get request data other than "magic" parameters.
726 *
727 * @return array<string, mixed> Request data, with magic filtered out.
728 */
729 public function getPassthroughRequestData($include_quicksand = false) {
730 $data = $this->getRequestData();
731
732 // Remove magic parameters like __dialog__ and __ajax__.
733 foreach ($data as $key => $value) {
734 if ($include_quicksand && $key == self::TYPE_QUICKSAND) {
735 continue;
736 }
737 if (!strncmp($key, '__', 2)) {
738 unset($data[$key]);
739 }
740 }
741
742 return $data;
743 }
744
745
746 /**
747 * Flatten an array of key-value pairs (possibly including arrays as values)
748 * into a list of key-value pairs suitable for submitting via HTTP request
749 * (with arrays flattened).
750 *
751 * @param array<string, mixed> $data Data to flatten.
752 * @return array<string, string> Flat data suitable for inclusion in an HTTP
753 * request.
754 */
755 public static function flattenData(array $data) {
756 $result = array();
757 foreach ($data as $key => $value) {
758 if (is_array($value)) {
759 foreach (self::flattenData($value) as $fkey => $fvalue) {
760 $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
761 $result[$key.$fkey] = $fvalue;
762 }
763 } else {
764 $result[$key] = (string)$value;
765 }
766 }
767
768 ksort($result);
769
770 return $result;
771 }
772
773
774 /**
775 * Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
776 *
777 * This function accepts a canonical header name, like `"Accept-Encoding"`,
778 * and looks up the appropriate value in `$_SERVER` (in this case,
779 * `"HTTP_ACCEPT_ENCODING"`).
780 *
781 * @param string $name Canonical header name, like `"Accept-Encoding"`.
782 * @param mixed $default (optional) Default value to return if
783 * header is not present.
784 * @param array $data (optional) Read this instead of `$_SERVER`.
785 * @return mixed Header value if present, or `$default` if not.
786 */
787 public static function getHTTPHeader($name, $default = null, $data = null) {
788 // PHP mangles HTTP headers by uppercasing them and replacing hyphens with
789 // underscores, then prepending 'HTTP_'.
790 $php_index = strtoupper($name);
791 $php_index = str_replace('-', '_', $php_index);
792
793 $try_names = array();
794
795 $try_names[] = 'HTTP_'.$php_index;
796 if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
797 // These headers may be available under alternate names. See
798 // https://www.php.net/manual/reserved.variables.server.php#110763
799 $try_names[] = $php_index;
800 }
801
802 if ($data === null) {
803 $data = $_SERVER;
804 }
805
806 foreach ($try_names as $try_name) {
807 if (array_key_exists($try_name, $data)) {
808 return $data[$try_name];
809 }
810 }
811
812 return $default;
813 }
814
815
816/* -( Working With a Phabricator Cluster )--------------------------------- */
817
818
819 /**
820 * Is this a proxied request originating from within the Phabricator cluster?
821 *
822 * IMPORTANT: This means the request is dangerous!
823 *
824 * These requests are **more dangerous** than normal requests (they can not
825 * be safely proxied, because proxying them may cause a loop). Cluster
826 * requests are not guaranteed to come from a trusted source, and should
827 * never be treated as safer than normal requests. They are strictly less
828 * safe.
829 */
830 public function isProxiedClusterRequest() {
831 return (bool)self::getHTTPHeader('X-Phabricator-Cluster');
832 }
833
834
835 /**
836 * Build a new @{class:HTTPSFuture} which proxies this request to another
837 * node in the cluster.
838 *
839 * IMPORTANT: This is very dangerous!
840 *
841 * The future forwards authentication information present in the request.
842 * Proxied requests must only be sent to trusted hosts. (We attempt to
843 * enforce this.)
844 *
845 * This is not a general-purpose proxying method; it is a specialized
846 * method with niche applications and severe security implications.
847 *
848 * @param string $uri URI identifying the host we are proxying the request
849 * to.
850 * @return HTTPSFuture New proxy future.
851 *
852 * @phutil-external-symbol class PhabricatorStartup
853 */
854 public function newClusterProxyFuture($uri) {
855 $uri = new PhutilURI($uri);
856
857 $domain = $uri->getDomain();
858 $ip = gethostbyname($domain);
859 if (!$ip) {
860 throw new Exception(
861 pht(
862 'Unable to resolve domain "%s"!',
863 $domain));
864 }
865
866 if (!PhabricatorEnv::isClusterAddress($ip)) {
867 throw new Exception(
868 pht(
869 'Refusing to proxy a request to IP address ("%s") which is not '.
870 'in the cluster address block (this address was derived by '.
871 'resolving the domain "%s").',
872 $ip,
873 $domain));
874 }
875
876 $uri->setPath($this->getPath());
877 $uri->removeAllQueryParams();
878 foreach (self::flattenData($_GET) as $query_key => $query_value) {
879 $uri->appendQueryParam($query_key, $query_value);
880 }
881
882 $input = PhabricatorStartup::getRawInput();
883
884 $future = id(new HTTPSFuture($uri))
885 ->addHeader('Host', self::getHost())
886 ->addHeader('X-Phabricator-Cluster', true)
887 ->setMethod($_SERVER['REQUEST_METHOD'])
888 ->write($input);
889
890 if (isset($_SERVER['PHP_AUTH_USER'])) {
891 $future->setHTTPBasicAuthCredentials(
892 $_SERVER['PHP_AUTH_USER'],
893 new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', '')));
894 }
895
896 $headers = array();
897
898 // NOTE: apache_request_headers() might provide a nicer way to do this,
899 // but isn't available under FCGI until PHP 5.4.0.
900 foreach ($_SERVER as $key => $value) {
901 if (!preg_match('/^HTTP_/', $key)) {
902 continue;
903 }
904
905 // Unmangle the header as best we can.
906 $key = substr($key, strlen('HTTP_'));
907 $key = str_replace('_', ' ', $key);
908 $key = strtolower($key);
909 $key = ucwords($key);
910 $key = str_replace(' ', '-', $key);
911
912 // Only forward "X-Hgarg-..." headers.
913 if (preg_match('/^X-Hgarg-/', $key)) {
914 $headers[] = array($key, $value);
915 }
916 }
917
918 // In some situations, this may not be mapped into the HTTP_X constants.
919 // CONTENT_LENGTH is similarly affected, but we trust cURL to take care
920 // of that if it matters, since we're handing off a request body.
921 if (isset($_SERVER['CONTENT_TYPE'])) {
922 $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']);
923 }
924
925 foreach ($headers as $header) {
926 list($key, $value) = $header;
927 $future->addHeader($key, $value);
928 }
929
930 return $future;
931 }
932
933 public function updateEphemeralCookies() {
934 $submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
935
936 $submit_key = $this->getCookie($submit_cookie);
937 if (phutil_nonempty_string($submit_key)) {
938 $this->clearCookie($submit_cookie);
939 $this->submitKey = $submit_key;
940 }
941
942 }
943
944 public function getSubmitKey() {
945 return $this->submitKey;
946 }
947
948}