@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
at upstream/main 948 lines 26 kB view raw
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}