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

Configure Feed

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

at recaptime-dev/main 1010 lines 30 kB view raw
1<?php 2 3/** 4 * Manages the execution environment configuration, exposing APIs to read 5 * configuration settings and other similar values that are derived directly 6 * from configuration settings. 7 * 8 * 9 * = Reading Configuration = 10 * 11 * The primary role of this class is to provide an API for reading 12 * Phabricator configuration, @{method:getEnvConfig}: 13 * 14 * $value = PhabricatorEnv::getEnvConfig('some.key', $default); 15 * 16 * The class also handles some URI construction based on configuration, via 17 * the methods @{method:getURI}, @{method:getProductionURI}, 18 * @{method:getCDNURI}, and @{method:getDoclink}. 19 * 20 * For configuration which allows you to choose a class to be responsible for 21 * some functionality (e.g., which mail adapter to use to deliver email), 22 * @{method:newObjectFromConfig} provides a simple interface that validates 23 * the configured value. 24 * 25 * 26 * = Unit Test Support = 27 * 28 * In unit tests, you can use @{method:beginScopedEnv} to create a temporary, 29 * mutable environment. The method returns a scope guard object which restores 30 * the environment when it is destroyed. For example: 31 * 32 * public function testExample() { 33 * $env = PhabricatorEnv::beginScopedEnv(); 34 * $env->overrideEnv('some.key', 'new-value-for-this-test'); 35 * 36 * // Some test which depends on the value of 'some.key'. 37 * 38 * } 39 * 40 * Your changes will persist until the `$env` object leaves scope or is 41 * destroyed. 42 * 43 * You should //not// use this in normal code. 44 * 45 * 46 * @task read Reading Configuration 47 * @task uri URI Validation 48 * @task test Unit Test Support 49 * @task internal Internals 50 */ 51final class PhabricatorEnv extends Phobject { 52 53 private static $sourceStack; 54 private static $repairSource; 55 private static $overrideSource; 56 private static $requestBaseURI; 57 private static $cache; 58 private static $localeCode; 59 private static $readOnly; 60 private static $readOnlyReason; 61 62 const READONLY_CONFIG = 'config'; 63 const READONLY_UNREACHABLE = 'unreachable'; 64 const READONLY_SEVERED = 'severed'; 65 const READONLY_MASTERLESS = 'masterless'; 66 67 /** 68 * @phutil-external-symbol class PhabricatorStartup 69 */ 70 public static function initializeWebEnvironment() { 71 self::initializeCommonEnvironment(false, false); 72 73 // Set up en_US locale for now so that, for instance, if you haven't 74 // set up your database at all it says "Run this command" rather than 75 // "Run these 1 command(s)" 76 // If there aren't any setup problems, then this will get overwritten with 77 // the logged-in user's locale or the locale specified in global default 78 // settings by PhabricatorAuthSessionEngine::willServeRequestForUser 79 // which is called from PhabricatorController:willBeginExecution 80 self::setLocaleCode('en_US'); 81 82 } 83 84 public static function initializeScriptEnvironment( 85 $config_optional, 86 $no_extensions) { 87 self::initializeCommonEnvironment($config_optional, $no_extensions); 88 89 // Set the default locale for command-line scripts 90 self::setLocaleCode(self::getEnvConfig('locale.command')); 91 92 // If a script has a --locale argument then go through our system for 93 // setting locales 94 PhutilArgumentParser::setLocaleCallback(array(__CLASS__, 'setLocaleCode')); 95 96 // NOTE: This is dangerous in general, but we know we're in a script context 97 // and are not vulnerable to CSRF. 98 AphrontWriteGuard::allowDangerousUnguardedWrites(true); 99 100 // There are several places where we log information (about errors, events, 101 // service calls, etc.) for analysis via DarkConsole or similar. These are 102 // useful for web requests, but grow unboundedly in long-running scripts and 103 // daemons. Discard data as it arrives in these cases. 104 PhutilServiceProfiler::getInstance()->enableDiscardMode(); 105 DarkConsoleErrorLogPluginAPI::enableDiscardMode(); 106 DarkConsoleEventPluginAPI::enableDiscardMode(); 107 } 108 109 110 private static function initializeCommonEnvironment( 111 $config_optional, 112 $no_extensions) { 113 PhutilErrorHandler::initialize(); 114 115 self::resetUmask(); 116 self::buildConfigurationSourceStack($config_optional, $no_extensions); 117 118 // Force a valid timezone. If both PHP and Phabricator configuration are 119 // invalid, use UTC. 120 $tz = self::getEnvConfig('phabricator.timezone'); 121 if ($tz) { 122 @date_default_timezone_set($tz); 123 } 124 $ok = @date_default_timezone_set(date_default_timezone_get()); 125 if (!$ok) { 126 date_default_timezone_set('UTC'); 127 } 128 129 // Prepend '/support/bin' and append any paths to $PATH if we need to. 130 $env_path = getenv('PATH'); 131 $phabricator_path = dirname(phutil_get_library_root('phabricator')); 132 $support_path = $phabricator_path.'/support/bin'; 133 $env_path = $support_path.PATH_SEPARATOR.$env_path; 134 $append_dirs = self::getEnvConfig('environment.append-paths'); 135 if (!empty($append_dirs)) { 136 $append_path = implode(PATH_SEPARATOR, $append_dirs); 137 $env_path = $env_path.PATH_SEPARATOR.$append_path; 138 } 139 putenv('PATH='.$env_path); 140 141 // Write this back into $_ENV, too, so ExecFuture picks it up when creating 142 // subprocess environments. 143 $_ENV['PATH'] = $env_path; 144 145 146 // If an instance identifier is defined, write it into the environment so 147 // it's available to subprocesses. 148 $instance = self::getEnvConfig('cluster.instance'); 149 if (phutil_nonempty_string($instance)) { 150 putenv('PHABRICATOR_INSTANCE='.$instance); 151 $_ENV['PHABRICATOR_INSTANCE'] = $instance; 152 } 153 154 PhabricatorEventEngine::initialize(); 155 156 // Load the preamble utility library if we haven't already. On web 157 // requests this loaded earlier, but we want to load it for non-web 158 // requests so that unit tests can call these functions. 159 require_once $phabricator_path.'/support/startup/preamble-utils.php'; 160 } 161 162 public static function beginScopedLocale($locale_code) { 163 return new PhabricatorLocaleScopeGuard($locale_code); 164 } 165 166 public static function getLocaleCode() { 167 return self::$localeCode; 168 } 169 170 public static function setLocaleCode($locale_code) { 171 if (!$locale_code) { 172 return; 173 } 174 175 if ($locale_code == self::$localeCode) { 176 return; 177 } 178 179 try { 180 $locale = PhutilLocale::loadLocale($locale_code); 181 $translations = PhutilTranslation::getTranslationMapForLocale( 182 $locale_code); 183 184 $override = self::getEnvConfig('translation.override'); 185 if (!is_array($override)) { 186 $override = array(); 187 } 188 189 PhutilTranslator::getInstance() 190 ->setLocale($locale) 191 ->setTranslations($override + $translations); 192 193 self::$localeCode = $locale_code; 194 } catch (Exception $ex) { 195 // Just ignore this; the user likely has an out-of-date locale code. 196 } 197 } 198 199 private static function buildConfigurationSourceStack( 200 $config_optional, $no_extensions) { 201 self::dropConfigCache(); 202 203 $stack = new PhabricatorConfigStackSource(); 204 self::$sourceStack = $stack; 205 206 $default_source = id(new PhabricatorConfigDefaultSource()) 207 ->setName(pht('Global Default')); 208 $stack->pushSource($default_source); 209 210 $env = self::getSelectedEnvironmentName(); 211 if ($env) { 212 $stack->pushSource( 213 id(new PhabricatorConfigFileSource($env)) 214 ->setName(pht("File '%s'", $env))); 215 } 216 217 $stack->pushSource( 218 id(new PhabricatorConfigLocalSource()) 219 ->setName(pht('Local Config'))); 220 221 // If the install overrides the database adapter, we might need to load 222 // the database adapter class before we can push on the database config. 223 // This config is locked and can't be edited from the web UI anyway. 224 if (!$no_extensions) { 225 foreach (self::getEnvConfig('load-libraries') as $library) { 226 phutil_load_library($library); 227 } 228 } 229 230 // Drop any class map caches, since they will have generated without 231 // any classes from libraries. Without this, preflight setup checks can 232 // cause generation of a setup check cache that omits checks defined in 233 // libraries, for example. 234 PhutilClassMapQuery::deleteCaches(); 235 236 // If custom libraries specify config options, they won't get default 237 // values as the Default source has already been loaded, so we get it to 238 // pull in all options from non-phabricator libraries now they are loaded. 239 $default_source->loadExternalOptions(); 240 241 // If this install has site config sources, load them now. 242 $site_sources = id(new PhutilClassMapQuery()) 243 ->setAncestorClass(PhabricatorConfigSiteSource::class) 244 ->setSortMethod('getPriority') 245 ->execute(); 246 247 foreach ($site_sources as $site_source) { 248 $stack->pushSource($site_source); 249 250 // If the site source did anything which reads config, throw it away 251 // to make sure any additional site sources get clean reads. 252 self::dropConfigCache(); 253 } 254 255 $masters = PhabricatorDatabaseRef::getMasterDatabaseRefs(); 256 if (!$masters) { 257 self::setReadOnly(true, self::READONLY_MASTERLESS); 258 } else { 259 // If any master is severed, we drop to readonly mode. In theory we 260 // could try to continue if we're only missing some applications, but 261 // this is very complex and we're unlikely to get it right. 262 263 foreach ($masters as $master) { 264 // Give severed masters one last chance to get healthy. 265 if ($master->isSevered()) { 266 $master->checkHealth(); 267 } 268 269 if ($master->isSevered()) { 270 self::setReadOnly(true, self::READONLY_SEVERED); 271 break; 272 } 273 } 274 } 275 276 try { 277 // See T13403. If we're starting up in "config optional" mode, suppress 278 // messages about connection retries. 279 if ($config_optional) { 280 $database_source = @new PhabricatorConfigDatabaseSource('default'); 281 } else { 282 $database_source = new PhabricatorConfigDatabaseSource('default'); 283 } 284 285 $database_source->setName(pht('Database')); 286 287 $stack->pushSource($database_source); 288 } catch (AphrontSchemaQueryException $exception) { 289 // If the database is not available, just skip this configuration 290 // source. This happens during `bin/storage upgrade`, `bin/conf` before 291 // schema setup, etc. 292 } catch (PhabricatorClusterStrandedException $ex) { 293 // This means we can't connect to any database host. That's fine as 294 // long as we're running a setup script like `bin/storage`. 295 if (!$config_optional) { 296 throw $ex; 297 } 298 } 299 300 // Drop the config cache one final time to make sure we're getting clean 301 // reads now that we've finished building the stack. 302 self::dropConfigCache(); 303 } 304 305 public static function repairConfig($key, $value) { 306 if (!self::$repairSource) { 307 self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) 308 ->setName(pht('Repaired Config')); 309 self::$sourceStack->pushSource(self::$repairSource); 310 } 311 self::$repairSource->setKeys(array($key => $value)); 312 self::dropConfigCache(); 313 } 314 315 public static function overrideConfig($key, $value) { 316 if (!self::$overrideSource) { 317 self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) 318 ->setName(pht('Overridden Config')); 319 self::$sourceStack->pushSource(self::$overrideSource); 320 } 321 self::$overrideSource->setKeys(array($key => $value)); 322 self::dropConfigCache(); 323 } 324 325 public static function getUnrepairedEnvConfig($key, $default = null) { 326 foreach (self::$sourceStack->getStack() as $source) { 327 if ($source === self::$repairSource) { 328 continue; 329 } 330 $result = $source->getKeys(array($key)); 331 if ($result) { 332 return $result[$key]; 333 } 334 } 335 return $default; 336 } 337 338 public static function getSelectedEnvironmentName() { 339 $env_var = 'PHABRICATOR_ENV'; 340 341 $env = idx($_SERVER, $env_var); 342 343 if (!$env) { 344 $env = getenv($env_var); 345 } 346 347 if (!$env) { 348 $env = idx($_ENV, $env_var); 349 } 350 351 if (!$env) { 352 $root = dirname(phutil_get_library_root('phabricator')); 353 $path = $root.'/conf/local/ENVIRONMENT'; 354 if (Filesystem::pathExists($path)) { 355 $env = trim(Filesystem::readFile($path)); 356 } 357 } 358 359 return $env; 360 } 361 362 363/* -( Reading Configuration )---------------------------------------------- */ 364 365 366 /** 367 * Get the current configuration setting for a given key. 368 * 369 * If the key is not found, then throw an Exception. 370 * 371 * @task read 372 */ 373 public static function getEnvConfig($key) { 374 if (!self::$sourceStack) { 375 throw new Exception( 376 pht( 377 'Trying to read configuration "%s" before configuration has been '. 378 'initialized.', 379 $key)); 380 } 381 382 if (isset(self::$cache[$key])) { 383 return self::$cache[$key]; 384 } 385 386 if (array_key_exists($key, self::$cache)) { 387 return self::$cache[$key]; 388 } 389 390 $result = self::$sourceStack->getKeys(array($key)); 391 if (array_key_exists($key, $result)) { 392 self::$cache[$key] = $result[$key]; 393 return $result[$key]; 394 } else { 395 throw new Exception( 396 pht( 397 "No config value specified for key '%s'.", 398 $key)); 399 } 400 } 401 402 /** 403 * Get the current configuration setting for a given key. If the key 404 * does not exist, return a default value instead of throwing. This is 405 * primarily useful for migrations involving keys which are slated for 406 * removal. 407 * 408 * @task read 409 */ 410 public static function getEnvConfigIfExists($key, $default = null) { 411 try { 412 return self::getEnvConfig($key); 413 } catch (Exception $ex) { 414 return $default; 415 } 416 } 417 418 419 /** 420 * Get the fully-qualified URI for a path. 421 * 422 * @task read 423 */ 424 public static function getURI($path) { 425 return rtrim(self::getAnyBaseURI(), '/').$path; 426 } 427 428 429 /** 430 * Get the fully-qualified production URI for a path. 431 * 432 * @task read 433 */ 434 public static function getProductionURI($path) { 435 // If we're passed a URI which already has a domain, simply return it 436 // unmodified. In particular, files may have URIs which point to a CDN 437 // domain. 438 $uri = new PhutilURI($path); 439 if ($uri->getDomain()) { 440 return $path; 441 } 442 443 $production_domain = self::getEnvConfig('phabricator.production-uri'); 444 if (!$production_domain) { 445 $production_domain = self::getAnyBaseURI(); 446 } 447 return rtrim($production_domain, '/').$path; 448 } 449 450 451 public static function isSelfURI($raw_uri) { 452 $uri = new PhutilURI($raw_uri); 453 454 $host = $uri->getDomain(); 455 if (!phutil_nonempty_string($host)) { 456 return true; 457 } 458 459 $host = phutil_utf8_strtolower($host); 460 461 $self_map = self::getSelfURIMap(); 462 return isset($self_map[$host]); 463 } 464 465 private static function getSelfURIMap() { 466 $self_uris = array(); 467 $self_uris[] = self::getProductionURI('/'); 468 $self_uris[] = self::getURI('/'); 469 470 $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); 471 foreach ($allowed_uris as $allowed_uri) { 472 $self_uris[] = $allowed_uri; 473 } 474 475 $self_map = array(); 476 foreach ($self_uris as $self_uri) { 477 $host = id(new PhutilURI($self_uri))->getDomain(); 478 if (!phutil_nonempty_string($host)) { 479 continue; 480 } 481 482 $host = phutil_utf8_strtolower($host); 483 $self_map[$host] = $host; 484 } 485 486 return $self_map; 487 } 488 489 /** 490 * Get the fully-qualified production URI for a static resource path. 491 * 492 * @task read 493 */ 494 public static function getCDNURI($path) { 495 $alt = self::getEnvConfig('security.alternate-file-domain'); 496 if (!$alt) { 497 $alt = self::getAnyBaseURI(); 498 } 499 $uri = new PhutilURI($alt); 500 $uri->setPath($path); 501 return (string)$uri; 502 } 503 504 505 /** 506 * Get the fully-qualified production URI for a documentation resource. 507 * 508 * @task read 509 */ 510 public static function getDoclink($resource, $type = 'article') { 511 $params = array( 512 'name' => $resource, 513 'type' => $type, 514 'jump' => true, 515 ); 516 517 $uri = new PhutilURI( 518 'https://we.phorge.it/diviner/find/', 519 $params); 520 521 return phutil_string_cast($uri); 522 } 523 524 525 /** 526 * Build a concrete object from a configuration key. 527 * 528 * @task read 529 */ 530 public static function newObjectFromConfig($key, $args = array()) { 531 $class = self::getEnvConfig($key); 532 return newv($class, $args); 533 } 534 535 public static function getAnyBaseURI() { 536 $base_uri = self::getEnvConfig('phabricator.base-uri'); 537 538 if (!$base_uri) { 539 $base_uri = self::getRequestBaseURI(); 540 } 541 542 if (!$base_uri) { 543 throw new Exception( 544 pht( 545 "Define '%s' in your configuration to continue.", 546 'phabricator.base-uri')); 547 } 548 549 return $base_uri; 550 } 551 552 public static function getRequestBaseURI() { 553 return self::$requestBaseURI; 554 } 555 556 public static function setRequestBaseURI($uri) { 557 self::$requestBaseURI = $uri; 558 } 559 560 public static function isReadOnly() { 561 if (self::$readOnly !== null) { 562 return self::$readOnly; 563 } 564 return self::getEnvConfig('cluster.read-only'); 565 } 566 567 public static function setReadOnly($read_only, $reason) { 568 self::$readOnly = $read_only; 569 self::$readOnlyReason = $reason; 570 } 571 572 public static function getReadOnlyMessage() { 573 $reason = self::getReadOnlyReason(); 574 switch ($reason) { 575 case self::READONLY_MASTERLESS: 576 return pht( 577 'This server is in read-only mode (no writable database '. 578 'is configured).'); 579 case self::READONLY_UNREACHABLE: 580 return pht( 581 'This server is in read-only mode (unreachable master).'); 582 case self::READONLY_SEVERED: 583 return pht( 584 'This server is in read-only mode (major interruption).'); 585 } 586 587 return pht('This server is in read-only mode.'); 588 } 589 590 public static function getReadOnlyURI() { 591 return urisprintf( 592 '/readonly/%s/', 593 self::getReadOnlyReason()); 594 } 595 596 public static function getReadOnlyReason() { 597 if (!self::isReadOnly()) { 598 return null; 599 } 600 601 if (self::$readOnlyReason !== null) { 602 return self::$readOnlyReason; 603 } 604 605 return self::READONLY_CONFIG; 606 } 607 608 609/* -( Unit Test Support )-------------------------------------------------- */ 610 611 612 /** 613 * @task test 614 */ 615 public static function beginScopedEnv() { 616 return new PhabricatorScopedEnv(self::pushTestEnvironment()); 617 } 618 619 620 /** 621 * @task test 622 */ 623 private static function pushTestEnvironment() { 624 self::dropConfigCache(); 625 $source = new PhabricatorConfigDictionarySource(array()); 626 self::$sourceStack->pushSource($source); 627 return spl_object_hash($source); 628 } 629 630 631 /** 632 * @task test 633 */ 634 public static function popTestEnvironment($key) { 635 self::dropConfigCache(); 636 $source = self::$sourceStack->popSource(); 637 $stack_key = spl_object_hash($source); 638 if ($stack_key !== $key) { 639 self::$sourceStack->pushSource($source); 640 throw new Exception( 641 pht( 642 'Scoped environments were destroyed in a different order than they '. 643 'were initialized.')); 644 } 645 } 646 647 648/* -( URI Validation )----------------------------------------------------- */ 649 650 651 /** 652 * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or 653 * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the 654 * URI of some other resource which has a valid protocol. This rejects 655 * garbage URIs and URIs with protocols which do not appear in the 656 * `uri.allowed-protocols` configuration, notably 'javascript:' URIs. 657 * 658 * NOTE: This method is generally intended to reject URIs which it may be 659 * unsafe to put in an "href" link attribute. 660 * 661 * @param string $uri URI to test. 662 * @return bool True if the URI identifies a web resource. 663 * @task uri 664 */ 665 public static function isValidURIForLink($uri) { 666 return self::isValidLocalURIForLink($uri) || 667 self::isValidRemoteURIForLink($uri); 668 } 669 670 671 /** 672 * Detect if a URI identifies some page on this server. 673 * 674 * NOTE: This method is generally intended to reject URIs which it may be 675 * unsafe to issue a "Location:" redirect to. 676 * 677 * @param string $uri URI to test. 678 * @return bool True if the URI identifies a local page. 679 * @task uri 680 */ 681 public static function isValidLocalURIForLink($uri) { 682 $uri = (string)$uri; 683 684 if (!phutil_nonempty_string($uri)) { 685 return false; 686 } 687 688 if (preg_match('/\s/', $uri)) { 689 // PHP hasn't been vulnerable to header injection attacks for a bunch of 690 // years, but we can safely reject these anyway since they're never valid. 691 return false; 692 } 693 694 // Chrome (at a minimum) interprets backslashes in Location headers and the 695 // URL bar as forward slashes. This is probably intended to reduce user 696 // error caused by confusion over which key is "forward slash" vs "back 697 // slash". 698 // 699 // However, it means a URI like "/\evil.com" is interpreted like 700 // "//evil.com", which is a protocol relative remote URI. 701 // 702 // Since we currently never generate URIs with backslashes in them, reject 703 // these unconditionally rather than trying to figure out how browsers will 704 // interpret them. 705 if (preg_match('/\\\\/', $uri)) { 706 return false; 707 } 708 709 // Valid URIs must begin with '/', followed by the end of the string or some 710 // other non-'/' character. This rejects protocol-relative URIs like 711 // "//evil.com/evil_stuff/". 712 return (bool)preg_match('@^/([^/]|$)@', $uri); 713 } 714 715 716 /** 717 * Detect if a URI identifies some valid linkable remote resource. 718 * 719 * @param string $uri URI to test. 720 * @return bool True if a URI identifies a remote resource with an allowed 721 * protocol. 722 * @task uri 723 */ 724 public static function isValidRemoteURIForLink($uri) { 725 try { 726 self::requireValidRemoteURIForLink($uri); 727 return true; 728 } catch (Exception $ex) { 729 return false; 730 } 731 } 732 733 734 /** 735 * Detect if a URI identifies a valid linkable remote resource, throwing a 736 * detailed message if it does not. 737 * 738 * A valid linkable remote resource can be safely linked or redirected to. 739 * This is primarily a protocol whitelist check. 740 * 741 * @param string $raw_uri URI to test. 742 * @return void 743 * @task uri 744 */ 745 public static function requireValidRemoteURIForLink($raw_uri) { 746 $uri = new PhutilURI($raw_uri); 747 748 $proto = $uri->getProtocol(); 749 if (!$proto) { 750 throw new Exception( 751 pht( 752 'URI "%s" is not a valid linkable resource. A valid linkable '. 753 'resource URI must specify a protocol.', 754 $raw_uri)); 755 } 756 757 $protocols = self::getEnvConfig('uri.allowed-protocols'); 758 if (!isset($protocols[$proto])) { 759 throw new Exception( 760 pht( 761 'URI "%s" is not a valid linkable resource. A valid linkable '. 762 'resource URI must use one of these protocols: %s.', 763 $raw_uri, 764 implode(', ', array_keys($protocols)))); 765 } 766 767 $domain = $uri->getDomain(); 768 if (!$domain) { 769 throw new Exception( 770 pht( 771 'URI "%s" is not a valid linkable resource. A valid linkable '. 772 'resource URI must specify a domain.', 773 $raw_uri)); 774 } 775 } 776 777 778 /** 779 * Detect if a URI identifies a valid fetchable remote resource. 780 * 781 * @param string $uri URI to test. 782 * @param list<string> $protocols Allowed protocols. 783 * @return bool True if the URI is a valid fetchable remote resource. 784 * @task uri 785 */ 786 public static function isValidRemoteURIForFetch($uri, array $protocols) { 787 try { 788 self::requireValidRemoteURIForFetch($uri, $protocols); 789 return true; 790 } catch (Exception $ex) { 791 return false; 792 } 793 } 794 795 796 /** 797 * Detect if a URI identifies a valid fetchable remote resource, throwing 798 * a detailed message if it does not. 799 * 800 * A valid fetchable remote resource can be safely fetched using a request 801 * originating on this server. This is a primarily an address check against 802 * the outbound address blacklist. 803 * 804 * @param string $raw_uri URI to test. 805 * @param list<string> $protocols Allowed protocols. 806 * @return array<string, string> Pre-resolved URI and domain. 807 * @task uri 808 */ 809 public static function requireValidRemoteURIForFetch( 810 $raw_uri, 811 array $protocols) { 812 813 $uri = new PhutilURI($raw_uri); 814 815 $proto = $uri->getProtocol(); 816 if (!$proto) { 817 throw new Exception( 818 pht( 819 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 820 'resource URI must specify a protocol.', 821 $raw_uri)); 822 } 823 824 $protocols = array_fuse($protocols); 825 if (!isset($protocols[$proto])) { 826 throw new Exception( 827 pht( 828 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 829 'resource URI must use one of these protocols: %s.', 830 $raw_uri, 831 implode(', ', array_keys($protocols)))); 832 } 833 834 $domain = $uri->getDomain(); 835 if (!$domain) { 836 throw new Exception( 837 pht( 838 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 839 'resource URI must specify a domain.', 840 $raw_uri)); 841 } 842 843 $addresses = gethostbynamel($domain); 844 if (!$addresses) { 845 throw new Exception( 846 pht( 847 'URI "%s" is not a valid fetchable resource. The domain "%s" could '. 848 'not be resolved.', 849 $raw_uri, 850 $domain)); 851 } 852 853 foreach ($addresses as $address) { 854 if (self::isBlacklistedOutboundAddress($address)) { 855 throw new Exception( 856 pht( 857 'URI "%s" is not a valid fetchable resource. The domain "%s" '. 858 'resolves to the address "%s", which is blacklisted for '. 859 'outbound requests.', 860 $raw_uri, 861 $domain, 862 $address)); 863 } 864 } 865 866 $resolved_uri = clone $uri; 867 $resolved_uri->setDomain(head($addresses)); 868 869 return array($resolved_uri, $domain); 870 } 871 872 873 /** 874 * Determine if an IP address is in the outbound address blacklist. 875 * 876 * @param string $address IP address. 877 * @return bool True if the address is blacklisted. 878 */ 879 public static function isBlacklistedOutboundAddress($address) { 880 $blacklist = self::getEnvConfig('security.outbound-blacklist'); 881 882 return PhutilCIDRList::newList($blacklist)->containsAddress($address); 883 } 884 885 public static function isClusterRemoteAddress() { 886 $cluster_addresses = self::getEnvConfig('cluster.addresses'); 887 if (!$cluster_addresses) { 888 return false; 889 } 890 891 $address = self::getRemoteAddress(); 892 if (!$address) { 893 throw new Exception( 894 pht( 895 'Unable to test remote address against cluster whitelist: '. 896 'REMOTE_ADDR is not defined or not valid.')); 897 } 898 899 return self::isClusterAddress($address); 900 } 901 902 public static function isClusterAddress($address) { 903 $cluster_addresses = self::getEnvConfig('cluster.addresses'); 904 if (!$cluster_addresses) { 905 throw new Exception( 906 pht( 907 'This server is not configured to serve cluster requests. '. 908 'Set `cluster.addresses` in the configuration to whitelist '. 909 'cluster hosts before sending requests that use a cluster '. 910 'authentication mechanism.')); 911 } 912 913 return PhutilCIDRList::newList($cluster_addresses) 914 ->containsAddress($address); 915 } 916 917 public static function getRemoteAddress() { 918 $address = idx($_SERVER, 'REMOTE_ADDR'); 919 if (!$address) { 920 return null; 921 } 922 923 try { 924 return PhutilIPAddress::newAddress($address); 925 } catch (Exception $ex) { 926 return null; 927 } 928 } 929 930/* -( Internals )---------------------------------------------------------- */ 931 932 933 /** 934 * @task internal 935 */ 936 public static function envConfigExists($key) { 937 return array_key_exists($key, self::$sourceStack->getKeys(array($key))); 938 } 939 940 941 /** 942 * @task internal 943 */ 944 public static function getAllConfigKeys() { 945 return self::$sourceStack->getAllKeys(); 946 } 947 948 public static function getConfigSourceStack() { 949 return self::$sourceStack; 950 } 951 952 /** 953 * @task internal 954 */ 955 public static function overrideTestEnvConfig($stack_key, $key, $value) { 956 $tmp = array(); 957 958 // If we don't have the right key, we'll throw when popping the last 959 // source off the stack. 960 do { 961 $source = self::$sourceStack->popSource(); 962 array_unshift($tmp, $source); 963 if (spl_object_hash($source) == $stack_key) { 964 $source->setKeys(array($key => $value)); 965 break; 966 } 967 } while (true); 968 969 foreach ($tmp as $source) { 970 self::$sourceStack->pushSource($source); 971 } 972 973 self::dropConfigCache(); 974 } 975 976 private static function dropConfigCache() { 977 self::$cache = array(); 978 } 979 980 private static function resetUmask() { 981 // Reset the umask to the common standard umask. The umask controls default 982 // permissions when files are created and propagates to subprocesses. 983 984 // "022" is the most common umask, but sometimes it is set to something 985 // unusual by the calling environment. 986 987 // Since various things rely on this umask to work properly and we are 988 // not aware of any legitimate reasons to adjust it, unconditionally 989 // normalize it until such reasons arise. See T7475 for discussion. 990 umask(022); 991 } 992 993 994 /** 995 * Get the path to an empty directory which is readable by all of the system 996 * user accounts that Phabricator acts as. 997 * 998 * In some cases, a binary needs some valid HOME or CWD to continue, but not 999 * all user accounts have valid home directories and even if they do they 1000 * may not be readable after a `sudo` operation. 1001 * 1002 * @return string Path to an empty directory suitable for use as a CWD. 1003 */ 1004 public static function getEmptyCWD() { 1005 $root = dirname(phutil_get_library_root('phabricator')); 1006 return $root.'/support/empty/'; 1007 } 1008 1009 1010}