Example #1
0
 $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')) {
Example #2
0
 /**
  * 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();
 }