$device = null; $is_cluster_request = false; if ($user_name && $device_name) { throw new Exception(pht('The %s and %s flags are mutually exclusive. You can not ' . 'authenticate as both a user ("%s") and a device ("%s"). ' . 'Specify one or the other, but not both.', '--phabricator-ssh-user', '--phabricator-ssh-device', $user_name, $device_name)); } else { if (strlen($user_name)) { $user = id(new PhabricatorPeopleQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withUsernames(array($user_name))->executeOne(); if (!$user) { throw new Exception(pht('Invalid username ("%s"). There is no user with this username.', $user_name)); } } else { if (strlen($device_name)) { if (!$remote_address) { throw new Exception(pht('Unable to identify remote address from the %s environment ' . 'variable. Device authentication is accepted only from trusted ' . 'sources.', 'SSH_CLIENT')); } if (!PhabricatorEnv::isClusterAddress($remote_address)) { throw new Exception(pht('This request originates from outside of the Phabricator cluster ' . 'address range. Requests signed with a trusted device key must ' . 'originate from trusted hosts.')); } $device = id(new AlmanacDeviceQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withNames(array($device_name))->executeOne(); if (!$device) { throw new Exception(pht('Invalid device name ("%s"). There is no device with this name.', $device->getName())); } // We're authenticated as a device, but we're going to read the user out of // the command below. $is_cluster_request = true; } else { throw new Exception(pht('This script must be invoked with either the %s or %s flag.', '--phabricator-ssh-user', '--phabricator-ssh-device')); } } } if ($args->getArg('ssh-command')) {
/** * Build a new @{class:HTTPSFuture} which proxies this request to another * node in the cluster. * * IMPORTANT: This is very dangerous! * * The future forwards authentication information present in the request. * Proxied requests must only be sent to trusted hosts. (We attempt to * enforce this.) * * This is not a general-purpose proxying method; it is a specialized * method with niche applications and severe security implications. * * @param string URI identifying the host we are proxying the request to. * @return HTTPSFuture New proxy future. * * @phutil-external-symbol class PhabricatorStartup */ public function newClusterProxyFuture($uri) { $uri = new PhutilURI($uri); $domain = $uri->getDomain(); $ip = gethostbyname($domain); if (!$ip) { throw new Exception(pht('Unable to resolve domain "%s"!', $domain)); } if (!PhabricatorEnv::isClusterAddress($ip)) { throw new Exception(pht('Refusing to proxy a request to IP address ("%s") which is not ' . 'in the cluster address block (this address was derived by ' . 'resolving the domain "%s").', $ip, $domain)); } $uri->setPath($this->getPath()); $uri->setQueryParams(self::flattenData($_GET)); $input = PhabricatorStartup::getRawInput(); $future = id(new HTTPSFuture($uri))->addHeader('Host', self::getHost())->addHeader('X-Phabricator-Cluster', true)->setMethod($_SERVER['REQUEST_METHOD'])->write($input); if (isset($_SERVER['PHP_AUTH_USER'])) { $future->setHTTPBasicAuthCredentials($_SERVER['PHP_AUTH_USER'], new PhutilOpaqueEnvelope(idx($_SERVER, 'PHP_AUTH_PW', ''))); } $headers = array(); $seen = array(); // NOTE: apache_request_headers() might provide a nicer way to do this, // but isn't available under FCGI until PHP 5.4.0. foreach ($_SERVER as $key => $value) { if (preg_match('/^HTTP_/', $key)) { // Unmangle the header as best we can. $key = str_replace('_', ' ', $key); $key = strtolower($key); $key = ucwords($key); $key = str_replace(' ', '-', $key); $headers[] = array($key, $value); $seen[$key] = true; } } // In some situations, this may not be mapped into the HTTP_X constants. // CONTENT_LENGTH is similarly affected, but we trust cURL to take care // of that if it matters, since we're handing off a request body. if (empty($seen['Content-Type'])) { if (isset($_SERVER['CONTENT_TYPE'])) { $headers[] = array('Content-Type', $_SERVER['CONTENT_TYPE']); } } foreach ($headers as $header) { list($key, $value) = $header; switch ($key) { case 'Host': case 'Authorization': // Don't forward these headers, we've already handled them elsewhere. unset($headers[$key]); break; default: break; } } foreach ($headers as $header) { list($key, $value) = $header; $future->addHeader($key, $value); } return $future; }
/** * Using builtin and application routes, build the appropriate * @{class:AphrontController} class for the request. To route a request, we * first test if the HTTP_HOST is configured as a valid Phabricator URI. If * it isn't, we do a special check to see if it's a custom domain for a blog * in the Phame application and if that fails we error. Otherwise, we test * against all application routes from installed * @{class:PhabricatorApplication}s. * * If we match a route, we construct the controller it points at, build it, * and return it. * * If we fail to match a route, but the current path is missing a trailing * "/", we try routing the same path with a trailing "/" and do a redirect * if that has a valid route. The idea is to canoncalize URIs for consistency, * but avoid breaking noncanonical URIs that we can easily salvage. * * NOTE: We only redirect on GET. On POST, we'd drop parameters and most * likely mutate the request implicitly, and a bad POST usually indicates a * programming error rather than a sloppy typist. * * If the failing path already has a trailing "/", or we can't route the * version with a "/", we call @{method:build404Controller}, which build a * fallback @{class:AphrontController}. * * @return pair<AphrontController,dict> Controller and dictionary of request * parameters. * @task routing */ public final function buildController() { $request = $this->getRequest(); // If we're configured to operate in cluster mode, reject requests which // were not received on a cluster interface. // // For example, a host may have an internal address like "170.0.0.1", and // also have a public address like "51.23.95.16". Assuming the cluster // is configured on a range like "170.0.0.0/16", we want to reject the // requests received on the public interface. // // Ideally, nodes in a cluster should only be listening on internal // interfaces, but they may be configured in such a way that they also // listen on external interfaces, since this is easy to forget about or // get wrong. As a broad security measure, reject requests received on any // interfaces which aren't on the whitelist. $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); if ($cluster_addresses) { $server_addr = idx($_SERVER, 'SERVER_ADDR'); if (!$server_addr) { if (php_sapi_name() == 'cli') { // This is a command line script (probably something like a unit // test) so it's fine that we don't have SERVER_ADDR defined. } else { throw new AphrontUsageException(pht('No %s', 'SERVER_ADDR'), pht('Phabricator is configured to operate in cluster mode, but ' . '%s is not defined in the request context. Your webserver ' . 'configuration needs to forward %s to PHP so Phabricator can ' . 'reject requests received on external interfaces.', 'SERVER_ADDR', 'SERVER_ADDR')); } } else { if (!PhabricatorEnv::isClusterAddress($server_addr)) { throw new AphrontUsageException(pht('External Interface'), pht('Phabricator is configured in cluster mode and the address ' . 'this request was received on ("%s") is not whitelisted as ' . 'a cluster address.', $server_addr)); } } } if (PhabricatorEnv::getEnvConfig('security.require-https')) { if (!$request->isHTTPS()) { $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); $https_uri->setProtocol('https'); // In this scenario, we'll be redirecting to HTTPS using an absolute // URI, so we need to permit an external redirect. return $this->buildRedirectController($https_uri, true); } } $path = $request->getPath(); $host = $request->getHost(); $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $prod_uri = PhabricatorEnv::getEnvConfig('phabricator.production-uri'); $file_uri = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); $allowed_uris = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); $uris = array_merge(array($base_uri, $prod_uri), $allowed_uris); $cdn_routes = array('/res/', '/file/data/', '/file/xform/', '/phame/r/'); $host_match = false; foreach ($uris as $uri) { if ($host === id(new PhutilURI($uri))->getDomain()) { $host_match = true; break; } } if (!$host_match) { if ($host === id(new PhutilURI($file_uri))->getDomain()) { foreach ($cdn_routes as $route) { if (strncmp($path, $route, strlen($route)) == 0) { $host_match = true; break; } } } } // NOTE: If the base URI isn't defined yet, don't activate alternate // domains. if ($base_uri && !$host_match) { try { $blog = id(new PhameBlogQuery())->setViewer(new PhabricatorUser())->withDomain($host)->executeOne(); } catch (PhabricatorPolicyException $ex) { throw new Exception(pht('This blog is not visible to logged out users, so it can not be ' . 'visited from a custom domain.')); } if (!$blog) { if ($prod_uri && $prod_uri != $base_uri) { $prod_str = pht('%s or %s', $base_uri, $prod_uri); } else { $prod_str = $base_uri; } throw new Exception(pht('Specified domain %s is not configured for Phabricator ' . 'requests. Please use %s to visit this instance.', $host, $prod_str)); } // TODO: Make this more flexible and modular so any application can // do crazy stuff here if it wants. $path = '/phame/live/' . $blog->getID() . '/' . $path; } list($controller, $uri_data) = $this->buildControllerForPath($path); if (!$controller) { if (!preg_match('@/$@', $path)) { // If we failed to match anything but don't have a trailing slash, try // to add a trailing slash and issue a redirect if that resolves. list($controller, $uri_data) = $this->buildControllerForPath($path . '/'); // NOTE: For POST, just 404 instead of redirecting, since the redirect // will be a GET without parameters. if ($controller && !$request->isHTTPPost()) { $slash_uri = $request->getRequestURI()->setPath($path . '/'); $external = strlen($request->getRequestURI()->getDomain()); return $this->buildRedirectController($slash_uri, $external); } } return $this->build404Controller(); } return array($controller, $uri_data); }
/** * Using builtin and application routes, build the appropriate * @{class:AphrontController} class for the request. To route a request, we * first test if the HTTP_HOST is configured as a valid Phabricator URI. If * it isn't, we do a special check to see if it's a custom domain for a blog * in the Phame application and if that fails we error. Otherwise, we test * against all application routes from installed * @{class:PhabricatorApplication}s. * * If we match a route, we construct the controller it points at, build it, * and return it. * * If we fail to match a route, but the current path is missing a trailing * "/", we try routing the same path with a trailing "/" and do a redirect * if that has a valid route. The idea is to canoncalize URIs for consistency, * but avoid breaking noncanonical URIs that we can easily salvage. * * NOTE: We only redirect on GET. On POST, we'd drop parameters and most * likely mutate the request implicitly, and a bad POST usually indicates a * programming error rather than a sloppy typist. * * If the failing path already has a trailing "/", or we can't route the * version with a "/", we call @{method:build404Controller}, which build a * fallback @{class:AphrontController}. * * @return pair<AphrontController,dict> Controller and dictionary of request * parameters. * @task routing */ public final function buildController() { $request = $this->getRequest(); // If we're configured to operate in cluster mode, reject requests which // were not received on a cluster interface. // // For example, a host may have an internal address like "170.0.0.1", and // also have a public address like "51.23.95.16". Assuming the cluster // is configured on a range like "170.0.0.0/16", we want to reject the // requests received on the public interface. // // Ideally, nodes in a cluster should only be listening on internal // interfaces, but they may be configured in such a way that they also // listen on external interfaces, since this is easy to forget about or // get wrong. As a broad security measure, reject requests received on any // interfaces which aren't on the whitelist. $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); if ($cluster_addresses) { $server_addr = idx($_SERVER, 'SERVER_ADDR'); if (!$server_addr) { if (php_sapi_name() == 'cli') { // This is a command line script (probably something like a unit // test) so it's fine that we don't have SERVER_ADDR defined. } else { throw new AphrontUsageException(pht('No %s', 'SERVER_ADDR'), pht('Phabricator is configured to operate in cluster mode, but ' . '%s is not defined in the request context. Your webserver ' . 'configuration needs to forward %s to PHP so Phabricator can ' . 'reject requests received on external interfaces.', 'SERVER_ADDR', 'SERVER_ADDR')); } } else { if (!PhabricatorEnv::isClusterAddress($server_addr)) { throw new AphrontUsageException(pht('External Interface'), pht('Phabricator is configured in cluster mode and the address ' . 'this request was received on ("%s") is not whitelisted as ' . 'a cluster address.', $server_addr)); } } } $site = $this->buildSiteForRequest($request); if ($site->shouldRequireHTTPS()) { if (!$request->isHTTPS()) { $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); $https_uri->setProtocol('https'); // In this scenario, we'll be redirecting to HTTPS using an absolute // URI, so we need to permit an external redirect. return $this->buildRedirectController($https_uri, true); } } // TODO: Really, the Site should get more control here and be able to // do its own routing logic if it wants, but we don't need that for now. $path = $site->getPathForRouting($request); list($controller, $uri_data) = $this->buildControllerForPath($path); if (!$controller) { if (!preg_match('@/$@', $path)) { // If we failed to match anything but don't have a trailing slash, try // to add a trailing slash and issue a redirect if that resolves. list($controller, $uri_data) = $this->buildControllerForPath($path . '/'); // NOTE: For POST, just 404 instead of redirecting, since the redirect // will be a GET without parameters. if ($controller && !$request->isHTTPPost()) { $slash_uri = $request->getRequestURI()->setPath($path . '/'); $external = strlen($request->getRequestURI()->getDomain()); return $this->buildRedirectController($slash_uri, $external); } } return $this->build404Controller(); } return array($controller, $uri_data); }
/** * Build a controller to respond to the request. * * @return pair<AphrontController,dict> Controller and dictionary of request * parameters. * @task routing */ private final function buildController() { $request = $this->getRequest(); // If we're configured to operate in cluster mode, reject requests which // were not received on a cluster interface. // // For example, a host may have an internal address like "170.0.0.1", and // also have a public address like "51.23.95.16". Assuming the cluster // is configured on a range like "170.0.0.0/16", we want to reject the // requests received on the public interface. // // Ideally, nodes in a cluster should only be listening on internal // interfaces, but they may be configured in such a way that they also // listen on external interfaces, since this is easy to forget about or // get wrong. As a broad security measure, reject requests received on any // interfaces which aren't on the whitelist. $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); if ($cluster_addresses) { $server_addr = idx($_SERVER, 'SERVER_ADDR'); if (!$server_addr) { if (php_sapi_name() == 'cli') { // This is a command line script (probably something like a unit // test) so it's fine that we don't have SERVER_ADDR defined. } else { throw new AphrontMalformedRequestException(pht('No %s', 'SERVER_ADDR'), pht('Phabricator is configured to operate in cluster mode, but ' . '%s is not defined in the request context. Your webserver ' . 'configuration needs to forward %s to PHP so Phabricator can ' . 'reject requests received on external interfaces.', 'SERVER_ADDR', 'SERVER_ADDR')); } } else { if (!PhabricatorEnv::isClusterAddress($server_addr)) { throw new AphrontMalformedRequestException(pht('External Interface'), pht('Phabricator is configured in cluster mode and the address ' . 'this request was received on ("%s") is not whitelisted as ' . 'a cluster address.', $server_addr)); } } } $site = $this->buildSiteForRequest($request); if ($site->shouldRequireHTTPS()) { if (!$request->isHTTPS()) { $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); $https_uri->setProtocol('https'); // In this scenario, we'll be redirecting to HTTPS using an absolute // URI, so we need to permit an external redirect. return $this->buildRedirectController($https_uri, true); } } $maps = $site->getRoutingMaps(); $path = $request->getPath(); $result = $this->routePath($maps, $path); if ($result) { return $result; } // If we failed to match anything but don't have a trailing slash, try // to add a trailing slash and issue a redirect if that resolves. // NOTE: We only do this for GET, since redirects switch to GET and drop // data like POST parameters. if (!preg_match('@/$@', $path) && $request->isHTTPGet()) { $result = $this->routePath($maps, $path . '/'); if ($result) { $slash_uri = $request->getRequestURI()->setPath($path . '/'); // We need to restore URI encoding because the webserver has // interpreted it. For example, this allows us to redirect a path // like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be // resolved meaningfully by an application. $slash_uri = phutil_escape_uri($slash_uri); $external = strlen($request->getRequestURI()->getDomain()); return $this->buildRedirectController($slash_uri, $external); } } return $this->build404Controller(); }