diff --git a/resources/sql/autopatches/20140731.cancdn.php b/resources/sql/autopatches/20140731.cancdn.php new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140731.cancdn.php @@ -0,0 +1,20 @@ +establishConnection('w'); +foreach (new LiskMigrationIterator($table) as $file) { + $id = $file->getID(); + echo "Updating flags for file {$id}...\n"; + $meta = $file->getMetadata(); + if (!idx($meta, 'canCDN')) { + + $meta['canCDN'] = true; + + queryfx( + $conn_w, + 'UPDATE %T SET metadata = %s WHERE id = %d', + $table->getTableName(), + json_encode($meta), + $id); + } +} diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -52,6 +52,8 @@ 'delete/(?P[1-9]\d*)/' => 'PhabricatorFileDeleteController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorFileEditController', 'info/(?P[^/]+)/' => 'PhabricatorFileInfoController', + 'data/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/.*' + => 'PhabricatorFileDataController', 'data/(?P[^/]+)/(?P[^/]+)/.*' => 'PhabricatorFileDataController', 'proxy/' => 'PhabricatorFileProxyController', diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -4,47 +4,118 @@ private $phid; private $key; + private $token; public function willProcessRequest(array $data) { $this->phid = $data['phid']; $this->key = $data['key']; + $this->token = idx($data, 'token'); } public function shouldRequireLogin() { return false; } + protected function checkFileAndToken($file) { + if (!$file) { + return new Aphront404Response(); + } + + if (!$file->validateSecretKey($this->key)) { + return new Aphront403Response(); + } + + return null; + } + public function processRequest() { $request = $this->getRequest(); $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); - $uri = new PhutilURI($alt); - $alt_domain = $uri->getDomain(); - if ($alt_domain && ($alt_domain != $request->getHost())) { + $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); + $alt_uri = new PhutilURI($alt); + $alt_domain = $alt_uri->getDomain(); + $req_domain = $request->getHost(); + $main_domain = id(new PhutilURI($base_uri))->getDomain(); + + $cache_response = true; + + if (empty($alt) || $main_domain == $alt_domain) { + // Alternate files domain isn't configured or it's set + // to the same as the default domain + + // load the file with permissions checks; + $file = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withPHIDs(array($this->phid)) + ->executeOne(); + + $error_response = $this->checkFileAndToken($file); + if ($error_response) { + return $error_response; + } + + // when the file is not CDNable, don't allow cache + $cache_response = $file->getCanCDN(); + } else if ($req_domain != $alt_domain) { + // Alternate domain is configured but this request isn't using it + + // load the file with permissions checks; + $file = id(new PhabricatorFileQuery()) + ->setViewer($request->getUser()) + ->withPHIDs(array($this->phid)) + ->executeOne(); + + $error_response = $this->checkFileAndToken($file); + if ($error_response) { + return $error_response; + } + + // if the user can see the file, generate a token; + // redirect to the alt domain with the token; return id(new AphrontRedirectResponse()) - ->setURI($uri->setPath($request->getPath())); - } + ->setURI($file->getCDNURIWithToken()); - // NOTE: This endpoint will ideally be accessed via CDN or otherwise on - // a non-credentialed domain. Knowing the file's secret key gives you - // access, regardless of authentication on the request itself. + } else { + // We are using the alternate domain - $file = id(new PhabricatorFileQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs(array($this->phid)) - ->executeOne(); - if (!$file) { - return new Aphront404Response(); - } + // load the file, bypassing permission checks; + $file = id(new PhabricatorFileQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($this->phid)) + ->executeOne(); - if (!$file->validateSecretKey($this->key)) { - return new Aphront403Response(); + $error_response = $this->checkFileAndToken($file); + if ($error_response) { + return $error_response; + } + + if ($this->token) { + // validate the token, if it is valid, continue + $validated_token = $file->validateOneTimeToken($this->token); + + if (!$validated_token) { + return new Aphront403Response(); + } + // return the file data without cache headers + $cache_response = false; + } else if (!$file->getCanCDN()) { + // file cannot be served via cdn, and no token given + // redirect to the main domain to aquire a token + $file_uri = id(new PhutilURI($file->getViewURI())) + ->setDomain($main_domain); + + return id(new AphrontRedirectResponse()) + ->setURI($file_uri); + } } $data = $file->loadFileData(); $response = new AphrontFileResponse(); $response->setContent($data); - $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); + if ($cache_response) { + $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); + } // NOTE: It's important to accept "Range" requests when playing audio. // If we don't, Safari has difficulty figuring out how long sounds are @@ -58,6 +129,11 @@ $response->setHTTPResponseCode(206); $response->setRange((int)$matches[1], (int)$matches[2]); } + } else if (isset($validated_token)) { + // consume the one-time token if we have one. + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $validated_token->delete(); + unset($unguarded); } $is_viewable = $file->isViewableInBrowser(); diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -873,12 +873,16 @@ $key = Filesystem::readRandomCharacters(16); // Save the new secret. - return id(new PhabricatorAuthTemporaryToken()) - ->setObjectPHID($this->getPHID()) - ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE) - ->setTokenExpires(time() + phutil_units('1 hour in seconds')) - ->setTokenCode(PhabricatorHash::digest($key)) - ->save(); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $token = id(new PhabricatorAuthTemporaryToken()) + ->setObjectPHID($this->getPHID()) + ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE) + ->setTokenExpires(time() + phutil_units('1 hour in seconds')) + ->setTokenCode(PhabricatorHash::digest($key)) + ->save(); + unset($unguarded); + + return $key; } public function validateOneTimeToken($token_code) { @@ -887,7 +891,7 @@ ->withObjectPHIDs(array($this->getPHID())) ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE)) ->withExpired(false) - ->withTokenCodes(array($token_code)) + ->withTokenCodes(array(PhabricatorHash::digest($token_code))) ->executeOne(); return $token;