diff --git a/resources/sql/autopatches/20151110.daemonenvhash.sql b/resources/sql/autopatches/20151110.daemonenvhash.sql new file mode 100644 index 0000000000..bde4b21741 --- /dev/null +++ b/resources/sql/autopatches/20151110.daemonenvhash.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_daemon.daemon_log + DROP COLUMN envHash; + +ALTER TABLE {$NAMESPACE}_daemon.daemon_log + DROP COLUMN envInfo; diff --git a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php index a89590cc18..0b2bd8614e 100644 --- a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php +++ b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php @@ -1,198 +1,96 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_RUNNING) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); if (!$task_daemon) { $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd'); $summary = pht( 'You must start the Phabricator daemons to send email, rebuild '. 'search indexes, and do other background processing.'); $message = pht( 'The Phabricator daemons are not running, so Phabricator will not '. 'be able to perform background processing (including sending email, '. 'rebuilding search indexes, importing commits, cleaning up old data, '. 'and running builds).'. "\n\n". 'Use %s to start daemons. See %s for more information.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Managing Daemons with phd'))); $this->newIssue('daemons.not-running') ->setShortName(pht('Daemons Not Running')) ->setName(pht('Phabricator Daemons Are Not Running')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd start'); } $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); - $environment_hash = PhabricatorEnv::calculateEnvironmentHash(); $all_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); foreach ($all_daemons as $daemon) { if ($phd_user) { if ($daemon->getRunningAsUser() != $phd_user) { $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd'); $summary = pht( 'At least one daemon is currently running as a different '. 'user than configured in the Phabricator %s setting', 'phd.user'); $message = pht( 'A daemon is running as user %s while the Phabricator config '. 'specifies %s to be %s.'. "\n\n". 'Either adjust %s to match %s or start '. 'the daemons as the correct user. '. "\n\n". '%s Daemons will try to use %s to start as the configured user. '. 'Make sure that the user who starts %s has the correct '. 'sudo permissions to start %s daemons as %s', 'phd.user', 'phd.user', 'phd', 'sudo', 'phd', 'phd', phutil_tag('tt', array(), $daemon->getRunningAsUser()), phutil_tag('tt', array(), $phd_user), phutil_tag('tt', array(), $daemon->getRunningAsUser()), phutil_tag('tt', array(), $phd_user)); $this->newIssue('daemons.run-as-different-user') ->setName(pht('Daemons are running as the wrong user')) ->setSummary($summary) ->setMessage($message) ->addCommand('phabricator/ $ ./bin/phd restart'); } } - - if ($daemon->getEnvHash() != $environment_hash) { - $doc_href = PhabricatorEnv::getDocLink( - 'Managing Daemons with phd'); - - $summary = pht( - 'At least one daemon is currently running with different '. - 'configuration than the Phabricator web application.'); - - $list_section = null; - $env_info = $daemon->getEnvInfo(); - if ($env_info) { - $issues = PhabricatorEnv::compareEnvironmentInfo( - PhabricatorEnv::calculateEnvironmentInfo(), - $env_info); - - if ($issues) { - foreach ($issues as $key => $issue) { - $issues[$key] = phutil_tag('li', array(), $issue); - } - - $list_section = array( - pht( - 'The configurations differ in the following %s way(s):', - phutil_count($issues)), - phutil_tag( - 'ul', - array(), - $issues), - ); - } - } - - - $message = pht( - 'At least one daemon is currently running with a different '. - 'configuration (config checksum %s) than the web application '. - '(config checksum %s).'. - "\n\n%s". - 'This usually means that you have just made a configuration change '. - 'from the web UI, but have not yet restarted the daemons. You '. - 'need to restart the daemons after making configuration changes '. - 'so they will pick up the new values: until you do, they will '. - 'continue operating with the old settings.'. - "\n\n". - '(If you plan to make more changes, you can restart the daemons '. - 'once after you finish making all of your changes.)'. - "\n\n". - 'Use %s to restart daemons. You can find a list of running daemons '. - 'in the %s, which will also help you identify which daemon (or '. - 'daemons) have divergent configuration. For more information about '. - 'managing the daemons, see %s in the documentation.'. - "\n\n". - 'This can also happen if you use the %s environmental variable to '. - 'choose a configuration file, but the daemons run with a different '. - 'value than the web application. If restarting the daemons does '. - 'not resolve this issue and you use %s to select configuration, '. - 'check that it is set consistently.'. - "\n\n". - 'A third possible cause is that you run several machines, and '. - 'the %s configuration file differs between them. This file is '. - 'updated when you edit configuration from the CLI with %s. If '. - 'restarting the daemons does not resolve this issue and you '. - 'run multiple machines, check that all machines have identical '. - '%s configuration files.'. - "\n\n". - 'This issue is not severe, but usually indicates that something '. - 'is not configured the way you expect, and may cause the daemons '. - 'to exhibit different behavior than the web application does.', - - phutil_tag('tt', array(), substr($daemon->getEnvHash(), 0, 12)), - phutil_tag('tt', array(), substr($environment_hash, 0, 12)), - $list_section, - phutil_tag('tt', array(), 'bin/phd restart'), - phutil_tag( - 'a', - array( - 'href' => '/daemon/', - 'target' => '_blank', - ), - pht('Daemon Console')), - phutil_tag( - 'a', - array( - 'href' => $doc_href, - 'target' => '_blank', - ), - pht('Managing Daemons with phd')), - phutil_tag('tt', array(), 'PHABRICATOR_ENV'), - phutil_tag('tt', array(), 'PHABRICATOR_ENV'), - phutil_tag('tt', array(), 'phabricator/conf/local/local.json'), - phutil_tag('tt', array(), 'bin/config'), - phutil_tag('tt', array(), 'phabricator/conf/local/local.json')); - - $this->newIssue('daemons.need-restarting') - ->setName(pht('Daemons and Web Have Different Config')) - ->setSummary($summary) - ->setMessage($message) - ->addCommand('phabricator/ $ ./bin/phd restart'); - break; - } } } } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 92cafb9449..de087096fb 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -1,299 +1,303 @@ newIssue('config.unknown.'.$key) ->setShortName($short) ->setName($name) ->setSummary($summary); $stack = PhabricatorEnv::getConfigSourceStack(); $stack = $stack->getStack(); $found = array(); $found_local = false; $found_database = false; foreach ($stack as $source_key => $source) { $value = $source->getKeys(array($key)); if ($value) { $found[] = $source->getName(); if ($source instanceof PhabricatorConfigDatabaseSource) { $found_database = true; } if ($source instanceof PhabricatorConfigLocalSource) { $found_local = true; } } } $message = $message."\n\n".pht( 'This configuration value is defined in these %d '. 'configuration source(s): %s.', count($found), implode(', ', $found)); $issue->setMessage($message); if ($found_local) { $command = csprintf('phabricator/ $ ./bin/config delete %s', $key); $issue->addCommand($command); } if ($found_database) { $issue->addPhabricatorConfig($key); } } } /** * Return a map of deleted config options. Keys are option keys; values are * explanations of what happened to the option. */ public static function getAncientConfig() { $reason_auth = pht( 'This option has been migrated to the "Auth" application. Your old '. 'configuration is still in effect, but now stored in "Auth" instead of '. 'configuration. Going forward, you can manage authentication from '. 'the web UI.'); $auth_config = array( 'controller.oauth-registration', 'auth.password-auth-enabled', 'facebook.auth-enabled', 'facebook.registration-enabled', 'facebook.auth-permanent', 'facebook.application-id', 'facebook.application-secret', 'facebook.require-https-auth', 'github.auth-enabled', 'github.registration-enabled', 'github.auth-permanent', 'github.application-id', 'github.application-secret', 'google.auth-enabled', 'google.registration-enabled', 'google.auth-permanent', 'google.application-id', 'google.application-secret', 'ldap.auth-enabled', 'ldap.hostname', 'ldap.port', 'ldap.base_dn', 'ldap.search_attribute', 'ldap.search-first', 'ldap.username-attribute', 'ldap.real_name_attributes', 'ldap.activedirectory_domain', 'ldap.version', 'ldap.referrals', 'ldap.anonymous-user-name', 'ldap.anonymous-user-password', 'ldap.start-tls', 'disqus.auth-enabled', 'disqus.registration-enabled', 'disqus.auth-permanent', 'disqus.application-id', 'disqus.application-secret', 'phabricator.oauth-uri', 'phabricator.auth-enabled', 'phabricator.registration-enabled', 'phabricator.auth-permanent', 'phabricator.application-id', 'phabricator.application-secret', ); $ancient_config = array_fill_keys($auth_config, $reason_auth); $markup_reason = pht( 'Custom remarkup rules are now added by subclassing '. '%s or %s.', 'PhabricatorRemarkupCustomInlineRule', 'PhabricatorRemarkupCustomBlockRule'); $session_reason = pht( 'Sessions now expire and are garbage collected rather than having an '. 'arbitrary concurrency limit.'); $differential_field_reason = pht( 'All Differential fields are now managed through the configuration '. 'option "%s". Use that option to configure which fields are shown.', 'differential.fields'); $reply_domain_reason = pht( 'Individual application reply handler domains have been removed. '. 'Configure a reply domain with "%s".', 'metamta.reply-handler-domain'); $reply_handler_reason = pht( 'Reply handlers can no longer be overridden with configuration.'); $monospace_reason = pht( 'Phabricator no longer supports global customization of monospaced '. 'fonts.'); $public_mail_reason = pht( 'Inbound mail addresses are now configured for each application '. 'in the Applications tool.'); $gc_reason = pht( 'Garbage collectors are now configured with "%s".', 'bin/garbage set-policy'); $ancient_config += array( 'phid.external-loaders' => pht( 'External loaders have been replaced. Extend `%s` '. 'to implement new PHID and handle types.', 'PhabricatorPHIDType'), 'maniphest.custom-task-extensions-class' => pht( 'Maniphest fields are now loaded automatically. '. 'You can configure them with `%s`.', 'maniphest.fields'), 'maniphest.custom-fields' => pht( 'Maniphest fields are now defined in `%s`. '. 'Existing definitions have been migrated.', 'maniphest.custom-field-definitions'), 'differential.custom-remarkup-rules' => $markup_reason, 'differential.custom-remarkup-block-rules' => $markup_reason, 'auth.sshkeys.enabled' => pht( 'SSH keys are now actually useful, so they are always enabled.'), 'differential.anonymous-access' => pht( 'Phabricator now has meaningful global access controls. See `%s`.', 'policy.allow-public'), 'celerity.resource-path' => pht( 'An alternate resource map is no longer supported. Instead, use '. 'multiple maps. See T4222.'), 'metamta.send-immediately' => pht( 'Mail is now always delivered by the daemons.'), 'auth.sessions.conduit' => $session_reason, 'auth.sessions.web' => $session_reason, 'tokenizer.ondemand' => pht( 'Phabricator now manages typeahead strategies automatically.'), 'differential.revision-custom-detail-renderer' => pht( 'Obsolete; use standard rendering events instead.'), 'differential.show-host-field' => $differential_field_reason, 'differential.show-test-plan-field' => $differential_field_reason, 'differential.field-selector' => $differential_field_reason, 'phabricator.show-beta-applications' => pht( 'This option has been renamed to `%s` to emphasize the '. 'unfinished nature of many prototype applications. '. 'Your existing setting has been migrated.', 'phabricator.show-prototypes'), 'notification.user' => pht( 'The notification server no longer requires root permissions. Start '. 'the server as the user you want it to run under.'), 'notification.debug' => pht( 'Notifications no longer have a dedicated debugging mode.'), 'translation.provider' => pht( 'The translation implementation has changed and providers are no '. 'longer used or supported.'), 'config.mask' => pht( 'Use `%s` instead of this option.', 'config.hide'), 'phd.start-taskmasters' => pht( 'Taskmasters now use an autoscaling pool. You can configure the '. 'pool size with `%s`.', 'phd.taskmasters'), 'storage.engine-selector' => pht( 'Phabricator now automatically discovers available storage engines '. 'at runtime.'), 'storage.upload-size-limit' => pht( 'Phabricator now supports arbitrarily large files. Consult the '. 'documentation for configuration details.'), 'security.allow-outbound-http' => pht( 'This option has been replaced with the more granular option `%s`.', 'security.outbound-blacklist'), 'metamta.reply.show-hints' => pht( 'Phabricator no longer shows reply hints in mail.'), 'metamta.differential.reply-handler-domain' => $reply_domain_reason, 'metamta.diffusion.reply-handler-domain' => $reply_domain_reason, 'metamta.macro.reply-handler-domain' => $reply_domain_reason, 'metamta.maniphest.reply-handler-domain' => $reply_domain_reason, 'metamta.pholio.reply-handler-domain' => $reply_domain_reason, 'metamta.diffusion.reply-handler' => $reply_handler_reason, 'metamta.differential.reply-handler' => $reply_handler_reason, 'metamta.maniphest.reply-handler' => $reply_handler_reason, 'metamta.package.reply-handler' => $reply_handler_reason, 'metamta.precedence-bulk' => pht( 'Phabricator now always sends transaction mail with '. '"Precedence: bulk" to improve deliverability.'), 'style.monospace' => $monospace_reason, 'style.monospace.windows' => $monospace_reason, 'search.engine-selector' => pht( 'Phabricator now automatically discovers available search engines '. 'at runtime.'), 'metamta.files.public-create-email' => $public_mail_reason, 'metamta.maniphest.public-create-email' => $public_mail_reason, 'metamta.maniphest.default-public-author' => $public_mail_reason, 'metamta.paste.public-create-email' => $public_mail_reason, 'security.allow-conduit-act-as-user' => pht( 'Impersonating users over the API is no longer supported.'), 'feed.public' => pht('The framable public feed is no longer supported.'), 'auth.login-message' => pht( 'This configuration option has been replaced with a modular '. 'handler. See T9346.'), 'gcdaemon.ttl.herald-transcripts' => $gc_reason, 'gcdaemon.ttl.daemon-logs' => $gc_reason, 'gcdaemon.ttl.differential-parse-cache' => $gc_reason, 'gcdaemon.ttl.markup-cache' => $gc_reason, 'gcdaemon.ttl.task-archive' => $gc_reason, 'gcdaemon.ttl.general-cache' => $gc_reason, 'gcdaemon.ttl.conduit-logs' => $gc_reason, + + 'phd.variant-config' => pht( + 'This configuration is no longer relevant because daemons '. + 'restart automatically on configuration changes.'), ); return $ancient_config; } } diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php index 587194cd4d..12c29da616 100644 --- a/src/applications/config/option/PhabricatorPHDConfigOptions.php +++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php @@ -1,97 +1,90 @@ newOption('phd.pid-directory', 'string', '/var/tmp/phd/pid') ->setDescription( pht('Directory that phd should use to track running daemons.')), $this->newOption('phd.log-directory', 'string', '/var/tmp/phd/log') ->setDescription( pht('Directory that the daemons should use to store log files.')), $this->newOption('phd.taskmasters', 'int', 4) ->setSummary(pht('Maximum taskmaster daemon pool size.')) ->setDescription( pht( 'Maximum number of taskmaster daemons to run at once. Raising '. 'this can increase the maximum throughput of the task queue. The '. 'pool will automatically scale down when unutilized.')), $this->newOption('phd.verbose', 'bool', false) ->setBoolOptions( array( pht('Verbose mode'), pht('Normal mode'), )) ->setSummary(pht("Launch daemons in 'verbose' mode by default.")) ->setDescription( pht( "Launch daemons in 'verbose' mode by default. This creates a lot ". "of output, but can help debug issues. Daemons launched in debug ". "mode with '%s' are always launched in verbose mode. ". "See also '%s'.", 'phd debug', 'phd.trace')), $this->newOption('phd.user', 'string', null) ->setLocked(true) ->setSummary(pht('System user to run daemons as.')) ->setDescription( pht( 'Specify a system user to run the daemons as. Primarily, this '. 'user will own the working copies of any repositories that '. 'Phabricator imports or manages. This option is new and '. 'experimental.')), $this->newOption('phd.trace', 'bool', false) ->setBoolOptions( array( pht('Trace mode'), pht('Normal mode'), )) ->setSummary(pht("Launch daemons in 'trace' mode by default.")) ->setDescription( pht( "Launch daemons in 'trace' mode by default. This creates an ". "ENORMOUS amount of output, but can help debug issues. Daemons ". "launched in debug mode with '%s' are always launched in ". "trace mode. See also '%s'.", 'phd debug', 'phd.verbose')), - $this->newOption('phd.variant-config', 'list', array()) - ->setDescription( - pht( - 'Specify config keys that can safely vary between the web tier '. - 'and the daemons. Primarily, this is a way to suppress the '. - '"Daemons and Web Have Different Config" setup issue on a per '. - 'config key basis.')), $this->newOption('phd.garbage-collection', 'wild', array()) ->setLocked(true) ->setLockedMessage( pht( 'This option can not be edited from the web UI. Use %s to adjust '. 'garbage collector policies.', phutil_tag('tt', array(), 'bin/garbage set-policy'))) ->setSummary(pht('Retention policies for garbage collection.')) ->setDescription( pht( 'Customizes retention policies for garbage collectors.')), ); } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php index 60bf290ae5..32af8f6f13 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -1,191 +1,183 @@ getViewer(); $id = $request->getURIData('id'); $log = id(new PhabricatorDaemonLogQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->setAllowStatusWrites(true) ->executeOne(); if (!$log) { return new Aphront404Response(); } $events = id(new PhabricatorDaemonLogEvent())->loadAllWhere( 'logID = %d ORDER BY id DESC LIMIT 1000', $log->getID()); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Daemon %s', $log->getID())); $header = id(new PHUIHeaderView()) ->setHeader($log->getDaemon()); $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_STATE); $status = $log->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_UNKNOWN: $tag->setBackgroundColor(PHUITagView::COLOR_ORANGE); $tag->setName(pht('Unknown')); break; case PhabricatorDaemonLog::STATUS_RUNNING: $tag->setBackgroundColor(PHUITagView::COLOR_GREEN); $tag->setName(pht('Running')); break; case PhabricatorDaemonLog::STATUS_DEAD: $tag->setBackgroundColor(PHUITagView::COLOR_RED); $tag->setName(pht('Dead')); break; case PhabricatorDaemonLog::STATUS_WAIT: $tag->setBackgroundColor(PHUITagView::COLOR_BLUE); $tag->setName(pht('Waiting')); break; case PhabricatorDaemonLog::STATUS_EXITING: $tag->setBackgroundColor(PHUITagView::COLOR_YELLOW); $tag->setName(pht('Exiting')); break; case PhabricatorDaemonLog::STATUS_EXITED: $tag->setBackgroundColor(PHUITagView::COLOR_GREY); $tag->setName(pht('Exited')); break; } $header->addTag($tag); - $env_hash = PhabricatorEnv::calculateEnvironmentHash(); - if ($log->getEnvHash() != $env_hash) { - $tag = id(new PHUITagView()) - ->setType(PHUITagView::TYPE_STATE) - ->setBackgroundColor(PHUITagView::COLOR_YELLOW) - ->setName(pht('Stale Config')); - $header->addTag($tag); - } $properties = $this->buildPropertyListView($log); $event_view = id(new PhabricatorDaemonLogEventsView()) ->setUser($viewer) ->setEvents($events); $event_panel = new PHUIObjectBoxView(); $event_panel->setHeaderText(pht('Events')); $event_panel->appendChild($event_view); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $event_panel, ), array( 'title' => pht('Daemon Log'), )); } private function buildPropertyListView(PhabricatorDaemonLog $daemon) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer); $id = $daemon->getID(); $c_epoch = $daemon->getDateCreated(); $u_epoch = $daemon->getDateModified(); $unknown_time = PhabricatorDaemonLogQuery::getTimeUntilUnknown(); $dead_time = PhabricatorDaemonLogQuery::getTimeUntilDead(); $wait_time = PhutilDaemonHandle::getWaitBeforeRestart(); $details = null; $status = $daemon->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_RUNNING: $details = pht( 'This daemon is running normally and reported a status update '. 'recently (within %s).', phutil_format_relative_time($unknown_time)); break; case PhabricatorDaemonLog::STATUS_UNKNOWN: $details = pht( 'This daemon has not reported a status update recently (within %s). '. 'It may have exited abruptly. After %s, it will be presumed dead.', phutil_format_relative_time($unknown_time), phutil_format_relative_time($dead_time)); break; case PhabricatorDaemonLog::STATUS_DEAD: $details = pht( 'This daemon did not report a status update for %s. It is '. 'presumed dead. Usually, this indicates that the daemon was '. 'killed or otherwise exited abruptly with an error. You may '. 'need to restart it.', phutil_format_relative_time($dead_time)); break; case PhabricatorDaemonLog::STATUS_WAIT: $details = pht( 'This daemon is running normally and reported a status update '. 'recently (within %s). However, it encountered an error while '. 'doing work and is waiting a little while (%s) to resume '. 'processing. After encountering an error, daemons wait before '. 'resuming work to avoid overloading services.', phutil_format_relative_time($unknown_time), phutil_format_relative_time($wait_time)); break; case PhabricatorDaemonLog::STATUS_EXITING: $details = pht('This daemon is shutting down gracefully.'); break; case PhabricatorDaemonLog::STATUS_EXITED: $details = pht('This daemon exited normally and is no longer running.'); break; } $view->addProperty(pht('Status Details'), $details); $view->addProperty(pht('Daemon Class'), $daemon->getDaemon()); $view->addProperty(pht('Host'), $daemon->getHost()); $view->addProperty(pht('PID'), $daemon->getPID()); $view->addProperty(pht('Running as'), $daemon->getRunningAsUser()); $view->addProperty(pht('Started'), phabricator_datetime($c_epoch, $viewer)); $view->addProperty( pht('Seen'), pht( '%s ago (%s)', phutil_format_relative_time(time() - $u_epoch), phabricator_datetime($u_epoch, $viewer))); $argv = $daemon->getArgv(); if (is_array($argv)) { $argv = implode("\n", $argv); } $view->addProperty( pht('Argv'), phutil_tag( 'textarea', array( 'style' => 'width: 100%; height: 12em;', ), $argv)); $view->addProperty( pht('View Full Logs'), phutil_tag( 'tt', array(), "phabricator/ $ ./bin/phd log --id {$id}")); return $view; } } diff --git a/src/applications/daemon/event/PhabricatorDaemonEventListener.php b/src/applications/daemon/event/PhabricatorDaemonEventListener.php index db6d9a2eff..41324cc9c9 100644 --- a/src/applications/daemon/event/PhabricatorDaemonEventListener.php +++ b/src/applications/daemon/event/PhabricatorDaemonEventListener.php @@ -1,121 +1,119 @@ listen(PhutilDaemonHandle::EVENT_DID_LAUNCH); $this->listen(PhutilDaemonHandle::EVENT_DID_LOG); $this->listen(PhutilDaemonHandle::EVENT_DID_HEARTBEAT); $this->listen(PhutilDaemonHandle::EVENT_WILL_GRACEFUL); $this->listen(PhutilDaemonHandle::EVENT_WILL_EXIT); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhutilDaemonHandle::EVENT_DID_LAUNCH: $this->handleLaunchEvent($event); break; case PhutilDaemonHandle::EVENT_DID_HEARTBEAT: $this->handleHeartbeatEvent($event); break; case PhutilDaemonHandle::EVENT_DID_LOG: $this->handleLogEvent($event); break; case PhutilDaemonHandle::EVENT_WILL_GRACEFUL: $this->handleGracefulEvent($event); break; case PhutilDaemonHandle::EVENT_WILL_EXIT: $this->handleExitEvent($event); break; } } private function handleLaunchEvent(PhutilEvent $event) { $id = $event->getValue('id'); $current_user = posix_getpwuid(posix_geteuid()); $daemon = id(new PhabricatorDaemonLog()) ->setDaemonID($id) ->setDaemon($event->getValue('daemonClass')) ->setHost(php_uname('n')) ->setPID(getmypid()) ->setRunningAsUser($current_user['name']) - ->setEnvHash(PhabricatorEnv::calculateEnvironmentHash()) - ->setEnvInfo(PhabricatorEnv::calculateEnvironmentInfo()) ->setStatus(PhabricatorDaemonLog::STATUS_RUNNING) ->setArgv($event->getValue('argv')) ->setExplicitArgv($event->getValue('explicitArgv')) ->save(); $this->daemons[$id] = $daemon; } private function handleHeartbeatEvent(PhutilEvent $event) { $daemon = $this->getDaemon($event->getValue('id')); // Just update the timestamp. $daemon->save(); } private function handleLogEvent(PhutilEvent $event) { $daemon = $this->getDaemon($event->getValue('id')); // TODO: This is a bit awkward for historical reasons, clean it up after // removing Conduit. $message = $event->getValue('message'); $context = $event->getValue('context'); if (strlen($context) && $context !== $message) { $message = "({$context}) {$message}"; } $type = $event->getValue('type'); $message = phutil_utf8ize($message); id(new PhabricatorDaemonLogEvent()) ->setLogID($daemon->getID()) ->setLogType($type) ->setMessage((string)$message) ->setEpoch(time()) ->save(); switch ($type) { case 'WAIT': $current_status = PhabricatorDaemonLog::STATUS_WAIT; break; default: $current_status = PhabricatorDaemonLog::STATUS_RUNNING; break; } if ($current_status !== $daemon->getStatus()) { $daemon->setStatus($current_status)->save(); } } private function handleGracefulEvent(PhutilEvent $event) { $id = $event->getValue('id'); $daemon = $this->getDaemon($id); $daemon->setStatus(PhabricatorDaemonLog::STATUS_EXITING)->save(); } private function handleExitEvent(PhutilEvent $event) { $id = $event->getValue('id'); $daemon = $this->getDaemon($id); $daemon->setStatus(PhabricatorDaemonLog::STATUS_EXITED)->save(); unset($this->daemons[$id]); } private function getDaemon($id) { if (isset($this->daemons[$id])) { return $this->daemons[$id]; } throw new Exception(pht('No such daemon "%s"!', $id)); } } diff --git a/src/applications/daemon/storage/PhabricatorDaemonLog.php b/src/applications/daemon/storage/PhabricatorDaemonLog.php index 6573ea8e4c..2d005e33e0 100644 --- a/src/applications/daemon/storage/PhabricatorDaemonLog.php +++ b/src/applications/daemon/storage/PhabricatorDaemonLog.php @@ -1,88 +1,84 @@ array( 'argv' => self::SERIALIZATION_JSON, 'explicitArgv' => self::SERIALIZATION_JSON, - 'envInfo' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'daemon' => 'text255', 'host' => 'text255', 'pid' => 'uint32', 'runningAsUser' => 'text255?', - 'envHash' => 'bytes40', 'status' => 'text8', 'daemonID' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'status' => array( 'columns' => array('status'), ), 'dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_daemonID' => array( 'columns' => array('daemonID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getExplicitArgv() { $argv = $this->explicitArgv; if (!is_array($argv)) { return array(); } return $argv; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return null; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return PhabricatorPolicies::POLICY_ADMIN; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php index 8be2743bd1..6c96509505 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php @@ -1,88 +1,80 @@ daemonLogs = $daemon_logs; return $this; } public function render() { $rows = array(); if (!$this->user) { throw new PhutilInvalidStateException('setUser'); } - $env_hash = PhabricatorEnv::calculateEnvironmentHash(); $list = new PHUIObjectItemListView(); $list->setFlush(true); foreach ($this->daemonLogs as $log) { $id = $log->getID(); $epoch = $log->getDateCreated(); $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Daemon %s', $id)) ->setHeader($log->getDaemon()) ->setHref("/daemon/log/{$id}/") ->addIcon('none', phabricator_datetime($epoch, $this->user)); $status = $log->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_RUNNING: - if ($env_hash != $log->getEnvHash()) { - $item->setStatusIcon('fa-warning yellow'); - $item->addAttribute(pht( - 'This daemon is running with an out of date configuration and '. - 'should be restarted.')); - } else { - $item->setStatusIcon('fa-rocket green'); - $item->addAttribute(pht('This daemon is running.')); - } + $item->setStatusIcon('fa-rocket green'); + $item->addAttribute(pht('This daemon is running.')); break; case PhabricatorDaemonLog::STATUS_DEAD: $item->setStatusIcon('fa-warning red'); $item->addAttribute( pht( 'This daemon is lost or exited uncleanly, and is presumed '. 'dead.')); $item->addIcon('fa-times grey', pht('Dead')); break; case PhabricatorDaemonLog::STATUS_EXITING: $item->addAttribute(pht('This daemon is exiting.')); $item->addIcon('fa-check', pht('Exiting')); break; case PhabricatorDaemonLog::STATUS_EXITED: $item->setDisabled(true); $item->addAttribute(pht('This daemon exited cleanly.')); $item->addIcon('fa-check grey', pht('Exited')); break; case PhabricatorDaemonLog::STATUS_WAIT: $item->setStatusIcon('fa-clock-o blue'); $item->addAttribute( pht( 'This daemon encountered an error recently and is waiting a '. 'moment to restart.')); $item->addIcon('fa-clock-o grey', pht('Waiting')); break; case PhabricatorDaemonLog::STATUS_UNKNOWN: default: $item->setStatusIcon('fa-warning orange'); $item->addAttribute( pht( 'This daemon has not reported its status recently. It may '. 'have exited uncleanly.')); $item->addIcon('fa-warning', pht('Unknown')); break; } $list->addItem($item); } return $list; } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index b576e2f83b..694c793e0a 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,895 +1,795 @@ overrideEnv('some.key', 'new-value-for-this-test'); * * // Some test which depends on the value of 'some.key'. * * } * * Your changes will persist until the `$env` object leaves scope or is * destroyed. * * You should //not// use this in normal code. * * * @task read Reading Configuration * @task uri URI Validation * @task test Unit Test Support * @task internal Internals */ final class PhabricatorEnv extends Phobject { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; private static $localeCode; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(); } public static function initializeScriptEnvironment() { self::initializeCommonEnvironment(); // NOTE: This is dangerous in general, but we know we're in a script context // and are not vulnerable to CSRF. AphrontWriteGuard::allowDangerousUnguardedWrites(true); // There are several places where we log information (about errors, events, // service calls, etc.) for analysis via DarkConsole or similar. These are // useful for web requests, but grow unboundedly in long-running scripts and // daemons. Discard data as it arrives in these cases. PhutilServiceProfiler::getInstance()->enableDiscardMode(); DarkConsoleErrorLogPluginAPI::enableDiscardMode(); DarkConsoleEventPluginAPI::enableDiscardMode(); } private static function initializeCommonEnvironment() { PhutilErrorHandler::initialize(); self::buildConfigurationSourceStack(); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = self::getEnvConfig('phabricator.timezone'); if ($tz) { @date_default_timezone_set($tz); } $ok = @date_default_timezone_set(date_default_timezone_get()); if (!$ok) { date_default_timezone_set('UTC'); } // Prepend '/support/bin' and append any paths to $PATH if we need to. $env_path = getenv('PATH'); $phabricator_path = dirname(phutil_get_library_root('phabricator')); $support_path = $phabricator_path.'/support/bin'; $env_path = $support_path.PATH_SEPARATOR.$env_path; $append_dirs = self::getEnvConfig('environment.append-paths'); if (!empty($append_dirs)) { $append_path = implode(PATH_SEPARATOR, $append_dirs); $env_path = $env_path.PATH_SEPARATOR.$append_path; } putenv('PATH='.$env_path); // Write this back into $_ENV, too, so ExecFuture picks it up when creating // subprocess environments. $_ENV['PATH'] = $env_path; // If an instance identifier is defined, write it into the environment so // it's available to subprocesses. $instance = self::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; } PhabricatorEventEngine::initialize(); // TODO: Add a "locale.default" config option once we have some reasonable // defaults which aren't silly nonsense. self::setLocaleCode('en_US'); } public static function beginScopedLocale($locale_code) { return new PhabricatorLocaleScopeGuard($locale_code); } public static function getLocaleCode() { return self::$localeCode; } public static function setLocaleCode($locale_code) { if (!$locale_code) { return; } if ($locale_code == self::$localeCode) { return; } try { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); $override = self::getEnvConfig('translation.override'); if (!is_array($override)) { $override = array(); } PhutilTranslator::getInstance() ->setLocale($locale) ->setTranslations($override + $translations); self::$localeCode = $locale_code; } catch (Exception $ex) { // Just ignore this; the user likely has an out-of-date locale code. } } private static function buildConfigurationSourceStack() { self::dropConfigCache(); $stack = new PhabricatorConfigStackSource(); self::$sourceStack = $stack; $default_source = id(new PhabricatorConfigDefaultSource()) ->setName(pht('Global Default')); $stack->pushSource($default_source); $env = self::getSelectedEnvironmentName(); if ($env) { $stack->pushSource( id(new PhabricatorConfigFileSource($env)) ->setName(pht("File '%s'", $env))); } $stack->pushSource( id(new PhabricatorConfigLocalSource()) ->setName(pht('Local Config'))); // If the install overrides the database adapter, we might need to load // the database adapter class before we can push on the database config. // This config is locked and can't be edited from the web UI anyway. foreach (self::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // If custom libraries specify config options, they won't get default // values as the Default source has already been loaded, so we get it to // pull in all options from non-phabricator libraries now they are loaded. $default_source->loadExternalOptions(); // If this install has site config sources, load them now. $site_sources = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSiteSource') ->setSortMethod('getPriority') ->execute(); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } } public static function repairConfig($key, $value) { if (!self::$repairSource) { self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Repaired Config')); self::$sourceStack->pushSource(self::$repairSource); } self::$repairSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function overrideConfig($key, $value) { if (!self::$overrideSource) { self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Overridden Config')); self::$sourceStack->pushSource(self::$overrideSource); } self::$overrideSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function getUnrepairedEnvConfig($key, $default = null) { foreach (self::$sourceStack->getStack() as $source) { if ($source === self::$repairSource) { continue; } $result = $source->getKeys(array($key)); if ($result) { return $result[$key]; } } return $default; } public static function getSelectedEnvironmentName() { $env_var = 'PHABRICATOR_ENV'; $env = idx($_SERVER, $env_var); if (!$env) { $env = getenv($env_var); } if (!$env) { $env = idx($_ENV, $env_var); } if (!$env) { $root = dirname(phutil_get_library_root('phabricator')); $path = $root.'/conf/local/ENVIRONMENT'; if (Filesystem::pathExists($path)) { $env = trim(Filesystem::readFile($path)); } } return $env; } - public static function calculateEnvironmentHash() { - $keys = self::getKeysForConsistencyCheck(); - - $values = array(); - foreach ($keys as $key) { - $values[$key] = self::getEnvConfigIfExists($key); - } - - return PhabricatorHash::digest(json_encode($values)); - } - - /** - * Returns a summary of non-default configuration settings to allow the - * "daemons and web have different config" setup check to list divergent - * keys. - */ - public static function calculateEnvironmentInfo() { - $keys = self::getKeysForConsistencyCheck(); - - $info = array(); - - $defaults = id(new PhabricatorConfigDefaultSource())->getAllKeys(); - foreach ($keys as $key) { - $current = self::getEnvConfigIfExists($key); - $default = idx($defaults, $key, null); - if ($current !== $default) { - $info[$key] = PhabricatorHash::digestForIndex(json_encode($current)); - } - } - - $keys_hash = array_keys($defaults); - sort($keys_hash); - $keys_hash = implode("\0", $keys_hash); - $keys_hash = PhabricatorHash::digestForIndex($keys_hash); - - return array( - 'version' => 1, - 'keys' => $keys_hash, - 'values' => $info, - ); - } - - - /** - * Compare two environment info summaries to generate a human-readable - * list of discrepancies. - */ - public static function compareEnvironmentInfo(array $u, array $v) { - $issues = array(); - - $uversion = idx($u, 'version'); - $vversion = idx($v, 'version'); - if ($uversion != $vversion) { - $issues[] = pht( - 'The two configurations were generated by different versions '. - 'of Phabricator.'); - - // These may not be comparable, so stop here. - return $issues; - } - - if ($u['keys'] !== $v['keys']) { - $issues[] = pht( - 'The two configurations have different keys. This usually means '. - 'that they are running different versions of Phabricator.'); - } - - $uval = idx($u, 'values', array()); - $vval = idx($v, 'values', array()); - - $all_keys = array_keys($uval + $vval); - - foreach ($all_keys as $key) { - $uv = idx($uval, $key); - $vv = idx($vval, $key); - if ($uv !== $vv) { - if ($uv && $vv) { - $issues[] = pht( - 'The configuration key "%s" is set in both configurations, but '. - 'set to different values.', - $key); - } else { - $issues[] = pht( - 'The configuration key "%s" is set in only one configuration.', - $key); - } - } - } - - return $issues; - } - - private static function getKeysForConsistencyCheck() { - $keys = array_keys(self::getAllConfigKeys()); - sort($keys); - - $skip_keys = self::getEnvConfig('phd.variant-config'); - return array_diff($keys, $skip_keys); - } - /* -( Reading Configuration )---------------------------------------------- */ /** * Get the current configuration setting for a given key. * * If the key is not found, then throw an Exception. * * @task read */ public static function getEnvConfig($key) { if (isset(self::$cache[$key])) { return self::$cache[$key]; } if (array_key_exists($key, self::$cache)) { return self::$cache[$key]; } $result = self::$sourceStack->getKeys(array($key)); if (array_key_exists($key, $result)) { self::$cache[$key] = $result[$key]; return $result[$key]; } else { throw new Exception( pht( "No config value specified for key '%s'.", $key)); } } /** * Get the current configuration setting for a given key. If the key * does not exist, return a default value instead of throwing. This is * primarily useful for migrations involving keys which are slated for * removal. * * @task read */ public static function getEnvConfigIfExists($key, $default = null) { try { return self::getEnvConfig($key); } catch (Exception $ex) { return $default; } } /** * Get the fully-qualified URI for a path. * * @task read */ public static function getURI($path) { return rtrim(self::getAnyBaseURI(), '/').$path; } /** * Get the fully-qualified production URI for a path. * * @task read */ public static function getProductionURI($path) { // If we're passed a URI which already has a domain, simply return it // unmodified. In particular, files may have URIs which point to a CDN // domain. $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $production_domain = self::getEnvConfig('phabricator.production-uri'); if (!$production_domain) { $production_domain = self::getAnyBaseURI(); } return rtrim($production_domain, '/').$path; } public static function getAllowedURIs($path) { $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); $return = array(); foreach ($allowed_uris as $allowed_uri) { $return[] = rtrim($allowed_uri, '/').$path; } return $return; } /** * Get the fully-qualified production URI for a static resource path. * * @task read */ public static function getCDNURI($path) { $alt = self::getEnvConfig('security.alternate-file-domain'); if (!$alt) { $alt = self::getAnyBaseURI(); } $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } /** * Get the fully-qualified production URI for a documentation resource. * * @task read */ public static function getDoclink($resource, $type = 'article') { $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); $uri->setQueryParam('name', $resource); $uri->setQueryParam('type', $type); $uri->setQueryParam('jump', true); return (string)$uri; } /** * Build a concrete object from a configuration key. * * @task read */ public static function newObjectFromConfig($key, $args = array()) { $class = self::getEnvConfig($key); return newv($class, $args); } public static function getAnyBaseURI() { $base_uri = self::getEnvConfig('phabricator.base-uri'); if (!$base_uri) { $base_uri = self::getRequestBaseURI(); } if (!$base_uri) { throw new Exception( pht( "Define '%s' in your configuration to continue.", 'phabricator.base-uri')); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } /* -( Unit Test Support )-------------------------------------------------- */ /** * @task test */ public static function beginScopedEnv() { return new PhabricatorScopedEnv(self::pushTestEnvironment()); } /** * @task test */ private static function pushTestEnvironment() { self::dropConfigCache(); $source = new PhabricatorConfigDictionarySource(array()); self::$sourceStack->pushSource($source); return spl_object_hash($source); } /** * @task test */ public static function popTestEnvironment($key) { self::dropConfigCache(); $source = self::$sourceStack->popSource(); $stack_key = spl_object_hash($source); if ($stack_key !== $key) { self::$sourceStack->pushSource($source); throw new Exception( pht( 'Scoped environments were destroyed in a different order than they '. 'were initialized.')); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the * URI of some other resource which has a valid protocol. This rejects * garbage URIs and URIs with protocols which do not appear in the * `uri.allowed-protocols` configuration, notably 'javascript:' URIs. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to put in an "href" link attribute. * * @param string URI to test. * @return bool True if the URI identifies a web resource. * @task uri */ public static function isValidURIForLink($uri) { return self::isValidLocalURIForLink($uri) || self::isValidRemoteURIForLink($uri); } /** * Detect if a URI identifies some page on this server. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to issue a "Location:" redirect to. * * @param string URI to test. * @return bool True if the URI identifies a local page. * @task uri */ public static function isValidLocalURIForLink($uri) { $uri = (string)$uri; if (!strlen($uri)) { return false; } if (preg_match('/\s/', $uri)) { // PHP hasn't been vulnerable to header injection attacks for a bunch of // years, but we can safely reject these anyway since they're never valid. return false; } // Chrome (at a minimum) interprets backslashes in Location headers and the // URL bar as forward slashes. This is probably intended to reduce user // error caused by confusion over which key is "forward slash" vs "back // slash". // // However, it means a URI like "/\evil.com" is interpreted like // "//evil.com", which is a protocol relative remote URI. // // Since we currently never generate URIs with backslashes in them, reject // these unconditionally rather than trying to figure out how browsers will // interpret them. if (preg_match('/\\\\/', $uri)) { return false; } // Valid URIs must begin with '/', followed by the end of the string or some // other non-'/' character. This rejects protocol-relative URIs like // "//evil.com/evil_stuff/". return (bool)preg_match('@^/([^/]|$)@', $uri); } /** * Detect if a URI identifies some valid linkable remote resource. * * @param string URI to test. * @return bool True if a URI idenfies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteURIForLink($uri) { try { self::requireValidRemoteURIForLink($uri); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid linkable remote resource, throwing a * detailed message if it does not. * * A valid linkable remote resource can be safely linked or redirected to. * This is primarily a protocol whitelist check. * * @param string URI to test. * @return void * @task uri */ public static function requireValidRemoteURIForLink($uri) { $uri = new PhutilURI($uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a protocol.', $uri)); } $protocols = self::getEnvConfig('uri.allowed-protocols'); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must use one of these protocols: %s.', $uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a domain.', $uri)); } } /** * Detect if a URI identifies a valid fetchable remote resource. * * @param string URI to test. * @param list Allowed protocols. * @return bool True if the URI is a valid fetchable remote resource. * @task uri */ public static function isValidRemoteURIForFetch($uri, array $protocols) { try { self::requireValidRemoteURIForFetch($uri, $protocols); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid fetchable remote resource, throwing * a detailed message if it does not. * * A valid fetchable remote resource can be safely fetched using a request * originating on this server. This is a primarily an address check against * the outbound address blacklist. * * @param string URI to test. * @param list Allowed protocols. * @return pair Pre-resolved URI and domain. * @task uri */ public static function requireValidRemoteURIForFetch( $uri, array $protocols) { $uri = new PhutilURI($uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a protocol.', $uri)); } $protocols = array_fuse($protocols); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must use one of these protocols: %s.', $uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a domain.', $uri)); } $addresses = gethostbynamel($domain); if (!$addresses) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" could '. 'not be resolved.', $uri, $domain)); } foreach ($addresses as $address) { if (self::isBlacklistedOutboundAddress($address)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" '. 'resolves to the address "%s", which is blacklisted for '. 'outbound requests.', $uri, $domain, $address)); } } $resolved_uri = clone $uri; $resolved_uri->setDomain(head($addresses)); return array($resolved_uri, $domain); } /** * Determine if an IP address is in the outbound address blacklist. * * @param string IP address. * @return bool True if the address is blacklisted. */ public static function isBlacklistedOutboundAddress($address) { $blacklist = self::getEnvConfig('security.outbound-blacklist'); return PhutilCIDRList::newList($blacklist)->containsAddress($address); } public static function isClusterRemoteAddress() { $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { throw new Exception( pht( 'Unable to test remote address against cluster whitelist: '. 'REMOTE_ADDR is not defined.')); } return self::isClusterAddress($address); } public static function isClusterAddress($address) { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { throw new Exception( pht( 'Phabricator is not configured to serve cluster requests. '. 'Set `cluster.addresses` in the configuration to whitelist '. 'cluster hosts before sending requests that use a cluster '. 'authentication mechanism.')); } return PhutilCIDRList::newList($cluster_addresses) ->containsAddress($address); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ public static function envConfigExists($key) { return array_key_exists($key, self::$sourceStack->getKeys(array($key))); } /** * @task internal */ public static function getAllConfigKeys() { return self::$sourceStack->getAllKeys(); } public static function getConfigSourceStack() { return self::$sourceStack; } /** * @task internal */ public static function overrideTestEnvConfig($stack_key, $key, $value) { $tmp = array(); // If we don't have the right key, we'll throw when popping the last // source off the stack. do { $source = self::$sourceStack->popSource(); array_unshift($tmp, $source); if (spl_object_hash($source) == $stack_key) { $source->setKeys(array($key => $value)); break; } } while (true); foreach ($tmp as $source) { self::$sourceStack->pushSource($source); } self::dropConfigCache(); } private static function dropConfigCache() { self::$cache = array(); } }