1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-19 03:01:11 +01:00

Don't require one-time tokens to view file resources

Summary:
Ref T10262. This removes one-time tokens and makes file data responses always-cacheable (for 30 days).

The URI will stop working once any attached object changes its view policy, or the file view policy itself changes.

Files with `canCDN` (totally public data like profile images, CSS, JS, etc) use "cache-control: public" so they can be CDN'd.

Files without `canCDN` use "cache-control: private" so they won't be cached by the CDN. They could still be cached by a misbehaving local cache, but if you don't want your users seeing one anothers' secret files you should configure your local network properly.

Our "Cache-Control" headers were also from 1999 or something, update them to be more modern/sane. I can't find any evidence that any browser has done the wrong thing with this simpler ruleset in the last ~10 years.

Test Plan:
  - Configured alternate file domain.
  - Viewed site: stuff worked.
  - Accessed a file on primary domain, got redirected to alternate domain.
  - Verified proper cache headers for `canCDN` (public) and non-`canCDN` (private) files.
  - Uploaded a file to a task, edited task policy, verified it scrambled the old URI.
  - Reloaded task, new URI generated transparently.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10262

Differential Revision: https://secure.phabricator.com/D15642
This commit is contained in:
epriestley 2016-04-06 13:06:34 -07:00
parent f9836cb646
commit 439821c7b2
10 changed files with 55 additions and 201 deletions

View file

@ -2378,7 +2378,6 @@ phutil_register_library_map(array(
'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php', 'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php', 'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php',
'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php', 'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php',
'PhabricatorFileAccessTemporaryTokenType' => 'applications/files/temporarytoken/PhabricatorFileAccessTemporaryTokenType.php',
'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php', 'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php',
'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php', 'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php',
'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php', 'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php',
@ -6842,7 +6841,6 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface', 'PhabricatorDestructibleInterface',
), ),
'PhabricatorFileAccessTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorFileBundleLoader' => 'Phobject', 'PhabricatorFileBundleLoader' => 'Phobject',
'PhabricatorFileChunk' => array( 'PhabricatorFileChunk' => array(
'PhabricatorFileDAO', 'PhabricatorFileDAO',

View file

@ -11,7 +11,6 @@ final class AphrontFileResponse extends AphrontResponse {
private $rangeMin; private $rangeMin;
private $rangeMax; private $rangeMax;
private $allowOrigins = array(); private $allowOrigins = array();
private $fileToken;
public function addAllowOrigin($origin) { public function addAllowOrigin($origin) {
$this->allowOrigins[] = $origin; $this->allowOrigins[] = $origin;
@ -76,15 +75,6 @@ final class AphrontFileResponse extends AphrontResponse {
return $this; return $this;
} }
public function setTemporaryFileToken(PhabricatorAuthTemporaryToken $token) {
$this->fileToken = $token;
return $this;
}
public function getTemporaryFileToken() {
return $this->fileToken;
}
public function getHeaders() { public function getHeaders() {
$headers = array( $headers = array(
array('Content-Type', $this->getMimeType()), array('Content-Type', $this->getMimeType()),
@ -128,15 +118,4 @@ final class AphrontFileResponse extends AphrontResponse {
return $headers; return $headers;
} }
public function didCompleteWrite($aborted) {
if (!$aborted) {
$token = $this->getTemporaryFileToken();
if ($token) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token->delete();
unset($unguarded);
}
}
}
} }

View file

@ -39,6 +39,11 @@ abstract class AphrontProxyResponse
return $this; return $this;
} }
public function setCanCDN($can_cdn) {
$this->getProxy()->setCanCDN($can_cdn);
return $this;
}
public function setLastModified($epoch_timestamp) { public function setLastModified($epoch_timestamp) {
$this->getProxy()->setLastModified($epoch_timestamp); $this->getProxy()->setLastModified($epoch_timestamp);
return $this; return $this;

View file

@ -4,6 +4,7 @@ abstract class AphrontResponse extends Phobject {
private $request; private $request;
private $cacheable = false; private $cacheable = false;
private $canCDN;
private $responseCode = 200; private $responseCode = 200;
private $lastModified = null; private $lastModified = null;
@ -66,6 +67,11 @@ abstract class AphrontResponse extends Phobject {
return $this; return $this;
} }
public function setCanCDN($can_cdn) {
$this->canCDN = $can_cdn;
return $this;
}
public function setLastModified($epoch_timestamp) { public function setLastModified($epoch_timestamp) {
$this->lastModified = $epoch_timestamp; $this->lastModified = $epoch_timestamp;
return $this; return $this;
@ -186,6 +192,18 @@ abstract class AphrontResponse extends Phobject {
public function getCacheHeaders() { public function getCacheHeaders() {
$headers = array(); $headers = array();
if ($this->cacheable) { if ($this->cacheable) {
if ($this->canCDN) {
$headers[] = array(
'Cache-Control',
'public',
);
} else {
$headers[] = array(
'Cache-Control',
'private',
);
}
$headers[] = array( $headers[] = array(
'Expires', 'Expires',
$this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable), $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),
@ -193,11 +211,7 @@ abstract class AphrontResponse extends Phobject {
} else { } else {
$headers[] = array( $headers[] = array(
'Cache-Control', 'Cache-Control',
'private, no-cache, no-store, must-revalidate', 'no-store',
);
$headers[] = array(
'Pragma',
'no-cache',
); );
$headers[] = array( $headers[] = array(
'Expires', 'Expires',

View file

@ -140,6 +140,7 @@ abstract class CelerityResourceController extends PhabricatorController {
private function makeResponseCacheable(AphrontResponse $response) { private function makeResponseCacheable(AphrontResponse $response) {
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30); $response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setLastModified(time()); $response->setLastModified(time());
$response->setCanCDN(true);
return $response; return $response;
} }

View file

@ -991,7 +991,7 @@ final class DiffusionServeController extends DiffusionController {
// <https://github.com/github/git-lfs/issues/1088> // <https://github.com/github/git-lfs/issues/1088>
$no_authorization = 'Basic '.base64_encode('none'); $no_authorization = 'Basic '.base64_encode('none');
$get_uri = $file->getCDNURIWithToken(); $get_uri = $file->getCDNURI();
$actions['download'] = array( $actions['download'] = array(
'href' => $get_uri, 'href' => $get_uri,
'header' => array( 'header' => array(

View file

@ -4,7 +4,6 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
private $phid; private $phid;
private $key; private $key;
private $token;
private $file; private $file;
public function shouldRequireLogin() { public function shouldRequireLogin() {
@ -15,7 +14,6 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$viewer = $request->getViewer(); $viewer = $request->getViewer();
$this->phid = $request->getURIData('phid'); $this->phid = $request->getURIData('phid');
$this->key = $request->getURIData('key'); $this->key = $request->getURIData('key');
$this->token = $request->getURIData('token');
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
@ -24,99 +22,40 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$req_domain = $request->getHost(); $req_domain = $request->getHost();
$main_domain = id(new PhutilURI($base_uri))->getDomain(); $main_domain = id(new PhutilURI($base_uri))->getDomain();
$cache_response = true;
if (empty($alt) || $main_domain == $alt_domain) { if (!strlen($alt) || $main_domain == $alt_domain) {
// Alternate files domain isn't configured or it's set // No alternate domain.
// to the same as the default domain $should_redirect = false;
$use_viewer = $viewer;
$response = $this->loadFile($viewer); $is_alternate_domain = false;
if ($response) {
return $response;
}
$file = $this->getFile();
// when the file is not CDNable, don't allow cache
$cache_response = $file->getCanCDN();
} else if ($req_domain != $alt_domain) { } else if ($req_domain != $alt_domain) {
// Alternate domain is configured but this request isn't using it // Alternate domain, but this request is on the main domain.
$should_redirect = true;
$use_viewer = $viewer;
$is_alternate_domain = false;
} else {
// Alternate domain, and on the alternate domain.
$should_redirect = false;
$use_viewer = PhabricatorUser::getOmnipotentUser();
$is_alternate_domain = true;
}
$response = $this->loadFile($viewer); $response = $this->loadFile($use_viewer);
if ($response) { if ($response) {
return $response; return $response;
} }
$file = $this->getFile();
// if the user can see the file, generate a token; $file = $this->getFile();
// redirect to the alt domain with the token;
$token_uri = $file->getCDNURIWithToken();
$token_uri = new PhutilURI($token_uri);
$token_uri = $this->addURIParameters($token_uri);
if ($should_redirect) {
return id(new AphrontRedirectResponse()) return id(new AphrontRedirectResponse())
->setIsExternal(true) ->setIsExternal(true)
->setURI($token_uri); ->setURI($file->getCDNURI());
} else {
// We are using the alternate domain. We don't have authentication
// on this domain, so we bypass policy checks when loading the file.
$bypass_policies = PhabricatorUser::getOmnipotentUser();
$response = $this->loadFile($bypass_policies);
if ($response) {
return $response;
}
$file = $this->getFile();
$acquire_token_uri = id(new PhutilURI($file->getViewURI()))
->setDomain($main_domain);
$acquire_token_uri = $this->addURIParameters($acquire_token_uri);
if ($this->token) {
// validate the token, if it is valid, continue
$validated_token = $file->validateOneTimeToken($this->token);
if (!$validated_token) {
$dialog = $this->newDialog()
->setShortTitle(pht('Expired File'))
->setTitle(pht('File Link Has Expired'))
->appendParagraph(
pht(
'The link you followed to view this file is invalid or '.
'expired.'))
->appendParagraph(
pht(
'Continue to generate a new link to the file. You may be '.
'required to log in.'))
->addCancelButton(
$acquire_token_uri,
pht('Continue'));
// Build an explicit response so we can respond with HTTP/403 instead
// of HTTP/200.
$response = id(new AphrontDialogResponse())
->setDialog($dialog)
->setHTTPResponseCode(403);
return $response;
}
// 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
// This is marked as an "external" URI because it is fully qualified.
return id(new AphrontRedirectResponse())
->setIsExternal(true)
->setURI($acquire_token_uri);
}
} }
$response = new AphrontFileResponse(); $response = new AphrontFileResponse();
if ($cache_response) { $response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30); $response->setCanCDN($file->getCanCDN());
}
$begin = null; $begin = null;
$end = null; $end = null;
@ -138,10 +77,6 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$response->setHTTPResponseCode(206); $response->setHTTPResponseCode(206);
$response->setRange($begin, ($end - 1)); $response->setRange($begin, ($end - 1));
} }
} else if (isset($validated_token)) {
// We set this on the response, and the response deletes it after the
// transfer completes. This allows transfers to be resumed, in theory.
$response->setTemporaryFileToken($validated_token);
} }
$is_viewable = $file->isViewableInBrowser(); $is_viewable = $file->isViewableInBrowser();
@ -150,7 +85,7 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
if ($is_viewable && !$force_download) { if ($is_viewable && !$force_download) {
$response->setMimeType($file->getViewableMimeType()); $response->setMimeType($file->getViewableMimeType());
} else { } else {
if (!$request->isHTTPPost() && !$alt_domain) { if (!$request->isHTTPPost() && !$is_alternate_domain) {
// NOTE: Require POST to download files from the primary domain. We'd // NOTE: Require POST to download files from the primary domain. We'd
// rather go full-bore and do a real CSRF check, but can't currently // rather go full-bore and do a real CSRF check, but can't currently
// authenticate users on the file domain. This should blunt any // authenticate users on the file domain. This should blunt any
@ -174,20 +109,6 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
return $response; return $response;
} }
/**
* Add passthrough parameters to the URI so they aren't lost when we
* redirect to acquire tokens.
*/
private function addURIParameters(PhutilURI $uri) {
$request = $this->getRequest();
if ($request->getBool('download')) {
$uri->setQueryParam('download', 1);
}
return $uri;
}
private function loadFile(PhabricatorUser $viewer) { private function loadFile(PhabricatorUser $viewer) {
$file = id(new PhabricatorFileQuery()) $file = id(new PhabricatorFileQuery())
->setViewer($viewer) ->setViewer($viewer)

View file

@ -697,10 +697,10 @@ final class PhabricatorFile extends PhabricatorFileDAO
pht('You must save a file before you can generate a view URI.')); pht('You must save a file before you can generate a view URI.'));
} }
return $this->getCDNURI(null); return $this->getCDNURI();
} }
private function getCDNURI($token) { public function getCDNURI() {
$name = self::normalizeFileName($this->getName()); $name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name); $name = phutil_escape_uri($name);
@ -720,9 +720,6 @@ final class PhabricatorFile extends PhabricatorFileDAO
$parts[] = $this->getSecretKey(); $parts[] = $this->getSecretKey();
$parts[] = $this->getPHID(); $parts[] = $this->getPHID();
if ($token) {
$parts[] = $token;
}
$parts[] = $name; $parts[] = $name;
$path = '/'.implode('/', $parts); $path = '/'.implode('/', $parts);
@ -737,19 +734,6 @@ final class PhabricatorFile extends PhabricatorFileDAO
} }
} }
/**
* Get the CDN URI for this file, including a one-time-use security token.
*
*/
public function getCDNURIWithToken() {
if (!$this->getPHID()) {
throw new Exception(
pht('You must save a file before you can generate a CDN URI.'));
}
return $this->getCDNURI($this->generateOneTimeToken());
}
public function getInfoURI() { public function getInfoURI() {
return '/'.$this->getMonogram(); return '/'.$this->getMonogram();
@ -1120,38 +1104,6 @@ final class PhabricatorFile extends PhabricatorFileDAO
return $this; return $this;
} }
protected function generateOneTimeToken() {
$key = Filesystem::readRandomCharacters(16);
$token_type = PhabricatorFileAccessTemporaryTokenType::TOKENTYPE;
// Save the new secret.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token = id(new PhabricatorAuthTemporaryToken())
->setTokenResource($this->getPHID())
->setTokenType($token_type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->save();
unset($unguarded);
return $key;
}
public function validateOneTimeToken($token_code) {
$token_type = PhabricatorFileAccessTemporaryTokenType::TOKENTYPE;
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokenResources(array($this->getPHID()))
->withTokenTypes(array($token_type))
->withExpired(false)
->withTokenCodes(array(PhabricatorHash::digest($token_code)))
->executeOne();
return $token;
}
/** /**
* Write the policy edge between this file and some object. * Write the policy edge between this file and some object.
* *

View file

@ -1,17 +0,0 @@
<?php
final class PhabricatorFileAccessTemporaryTokenType
extends PhabricatorAuthTemporaryTokenType {
const TOKENTYPE = 'file:onetime';
public function getTokenTypeDisplayName() {
return pht('File Access');
}
public function getTokenReadableTypeName(
PhabricatorAuthTemporaryToken $token) {
return pht('File Access Token');
}
}

View file

@ -32,6 +32,7 @@ final class PhabricatorRobotsController extends PhabricatorController {
return id(new AphrontPlainTextResponse()) return id(new AphrontPlainTextResponse())
->setContent($content) ->setContent($content)
->setCacheDurationInSeconds(phutil_units('2 hours in seconds')); ->setCacheDurationInSeconds(phutil_units('2 hours in seconds'))
->setCanCDN(true);
} }
} }