@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+55 -201
-2
src/__phutil_library_map__.php
··· 2378 2378 'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php', 2379 2379 'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php', 2380 2380 'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php', 2381 - 'PhabricatorFileAccessTemporaryTokenType' => 'applications/files/temporarytoken/PhabricatorFileAccessTemporaryTokenType.php', 2382 2381 'PhabricatorFileBundleLoader' => 'applications/files/query/PhabricatorFileBundleLoader.php', 2383 2382 'PhabricatorFileChunk' => 'applications/files/storage/PhabricatorFileChunk.php', 2384 2383 'PhabricatorFileChunkIterator' => 'applications/files/engine/PhabricatorFileChunkIterator.php', ··· 6842 6841 'PhabricatorPolicyInterface', 6843 6842 'PhabricatorDestructibleInterface', 6844 6843 ), 6845 - 'PhabricatorFileAccessTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 6846 6844 'PhabricatorFileBundleLoader' => 'Phobject', 6847 6845 'PhabricatorFileChunk' => array( 6848 6846 'PhabricatorFileDAO',
-21
src/aphront/response/AphrontFileResponse.php
··· 11 11 private $rangeMin; 12 12 private $rangeMax; 13 13 private $allowOrigins = array(); 14 - private $fileToken; 15 14 16 15 public function addAllowOrigin($origin) { 17 16 $this->allowOrigins[] = $origin; ··· 76 75 return $this; 77 76 } 78 77 79 - public function setTemporaryFileToken(PhabricatorAuthTemporaryToken $token) { 80 - $this->fileToken = $token; 81 - return $this; 82 - } 83 - 84 - public function getTemporaryFileToken() { 85 - return $this->fileToken; 86 - } 87 - 88 78 public function getHeaders() { 89 79 $headers = array( 90 80 array('Content-Type', $this->getMimeType()), ··· 126 116 127 117 $headers = array_merge(parent::getHeaders(), $headers); 128 118 return $headers; 129 - } 130 - 131 - public function didCompleteWrite($aborted) { 132 - if (!$aborted) { 133 - $token = $this->getTemporaryFileToken(); 134 - if ($token) { 135 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 136 - $token->delete(); 137 - unset($unguarded); 138 - } 139 - } 140 119 } 141 120 142 121 }
+5
src/aphront/response/AphrontProxyResponse.php
··· 39 39 return $this; 40 40 } 41 41 42 + public function setCanCDN($can_cdn) { 43 + $this->getProxy()->setCanCDN($can_cdn); 44 + return $this; 45 + } 46 + 42 47 public function setLastModified($epoch_timestamp) { 43 48 $this->getProxy()->setLastModified($epoch_timestamp); 44 49 return $this;
+19 -5
src/aphront/response/AphrontResponse.php
··· 4 4 5 5 private $request; 6 6 private $cacheable = false; 7 + private $canCDN; 7 8 private $responseCode = 200; 8 9 private $lastModified = null; 9 10 ··· 63 64 64 65 public function setCacheDurationInSeconds($duration) { 65 66 $this->cacheable = $duration; 67 + return $this; 68 + } 69 + 70 + public function setCanCDN($can_cdn) { 71 + $this->canCDN = $can_cdn; 66 72 return $this; 67 73 } 68 74 ··· 186 192 public function getCacheHeaders() { 187 193 $headers = array(); 188 194 if ($this->cacheable) { 195 + if ($this->canCDN) { 196 + $headers[] = array( 197 + 'Cache-Control', 198 + 'public', 199 + ); 200 + } else { 201 + $headers[] = array( 202 + 'Cache-Control', 203 + 'private', 204 + ); 205 + } 206 + 189 207 $headers[] = array( 190 208 'Expires', 191 209 $this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable), ··· 193 211 } else { 194 212 $headers[] = array( 195 213 'Cache-Control', 196 - 'private, no-cache, no-store, must-revalidate', 197 - ); 198 - $headers[] = array( 199 - 'Pragma', 200 - 'no-cache', 214 + 'no-store', 201 215 ); 202 216 $headers[] = array( 203 217 'Expires',
+1
src/applications/celerity/controller/CelerityResourceController.php
··· 140 140 private function makeResponseCacheable(AphrontResponse $response) { 141 141 $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); 142 142 $response->setLastModified(time()); 143 + $response->setCanCDN(true); 143 144 144 145 return $response; 145 146 }
+1 -1
src/applications/diffusion/controller/DiffusionServeController.php
··· 991 991 // <https://github.com/github/git-lfs/issues/1088> 992 992 $no_authorization = 'Basic '.base64_encode('none'); 993 993 994 - $get_uri = $file->getCDNURIWithToken(); 994 + $get_uri = $file->getCDNURI(); 995 995 $actions['download'] = array( 996 996 'href' => $get_uri, 997 997 'header' => array(
+25 -104
src/applications/files/controller/PhabricatorFileDataController.php
··· 4 4 5 5 private $phid; 6 6 private $key; 7 - private $token; 8 7 private $file; 9 8 10 9 public function shouldRequireLogin() { ··· 15 14 $viewer = $request->getViewer(); 16 15 $this->phid = $request->getURIData('phid'); 17 16 $this->key = $request->getURIData('key'); 18 - $this->token = $request->getURIData('token'); 19 17 20 18 $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); 21 19 $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); ··· 24 22 $req_domain = $request->getHost(); 25 23 $main_domain = id(new PhutilURI($base_uri))->getDomain(); 26 24 27 - $cache_response = true; 28 25 29 - if (empty($alt) || $main_domain == $alt_domain) { 30 - // Alternate files domain isn't configured or it's set 31 - // to the same as the default domain 32 - 33 - $response = $this->loadFile($viewer); 34 - if ($response) { 35 - return $response; 36 - } 37 - $file = $this->getFile(); 38 - 39 - // when the file is not CDNable, don't allow cache 40 - $cache_response = $file->getCanCDN(); 26 + if (!strlen($alt) || $main_domain == $alt_domain) { 27 + // No alternate domain. 28 + $should_redirect = false; 29 + $use_viewer = $viewer; 30 + $is_alternate_domain = false; 41 31 } else if ($req_domain != $alt_domain) { 42 - // Alternate domain is configured but this request isn't using it 32 + // Alternate domain, but this request is on the main domain. 33 + $should_redirect = true; 34 + $use_viewer = $viewer; 35 + $is_alternate_domain = false; 36 + } else { 37 + // Alternate domain, and on the alternate domain. 38 + $should_redirect = false; 39 + $use_viewer = PhabricatorUser::getOmnipotentUser(); 40 + $is_alternate_domain = true; 41 + } 43 42 44 - $response = $this->loadFile($viewer); 45 - if ($response) { 46 - return $response; 47 - } 48 - $file = $this->getFile(); 43 + $response = $this->loadFile($use_viewer); 44 + if ($response) { 45 + return $response; 46 + } 49 47 50 - // if the user can see the file, generate a token; 51 - // redirect to the alt domain with the token; 52 - $token_uri = $file->getCDNURIWithToken(); 53 - $token_uri = new PhutilURI($token_uri); 54 - $token_uri = $this->addURIParameters($token_uri); 48 + $file = $this->getFile(); 55 49 50 + if ($should_redirect) { 56 51 return id(new AphrontRedirectResponse()) 57 52 ->setIsExternal(true) 58 - ->setURI($token_uri); 59 - 60 - } else { 61 - // We are using the alternate domain. We don't have authentication 62 - // on this domain, so we bypass policy checks when loading the file. 63 - 64 - $bypass_policies = PhabricatorUser::getOmnipotentUser(); 65 - $response = $this->loadFile($bypass_policies); 66 - if ($response) { 67 - return $response; 68 - } 69 - $file = $this->getFile(); 70 - 71 - $acquire_token_uri = id(new PhutilURI($file->getViewURI())) 72 - ->setDomain($main_domain); 73 - $acquire_token_uri = $this->addURIParameters($acquire_token_uri); 74 - 75 - if ($this->token) { 76 - // validate the token, if it is valid, continue 77 - $validated_token = $file->validateOneTimeToken($this->token); 78 - 79 - if (!$validated_token) { 80 - $dialog = $this->newDialog() 81 - ->setShortTitle(pht('Expired File')) 82 - ->setTitle(pht('File Link Has Expired')) 83 - ->appendParagraph( 84 - pht( 85 - 'The link you followed to view this file is invalid or '. 86 - 'expired.')) 87 - ->appendParagraph( 88 - pht( 89 - 'Continue to generate a new link to the file. You may be '. 90 - 'required to log in.')) 91 - ->addCancelButton( 92 - $acquire_token_uri, 93 - pht('Continue')); 94 - 95 - // Build an explicit response so we can respond with HTTP/403 instead 96 - // of HTTP/200. 97 - $response = id(new AphrontDialogResponse()) 98 - ->setDialog($dialog) 99 - ->setHTTPResponseCode(403); 100 - 101 - return $response; 102 - } 103 - // return the file data without cache headers 104 - $cache_response = false; 105 - } else if (!$file->getCanCDN()) { 106 - // file cannot be served via cdn, and no token given 107 - // redirect to the main domain to aquire a token 108 - 109 - // This is marked as an "external" URI because it is fully qualified. 110 - return id(new AphrontRedirectResponse()) 111 - ->setIsExternal(true) 112 - ->setURI($acquire_token_uri); 113 - } 53 + ->setURI($file->getCDNURI()); 114 54 } 115 55 116 56 $response = new AphrontFileResponse(); 117 - if ($cache_response) { 118 - $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); 119 - } 57 + $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); 58 + $response->setCanCDN($file->getCanCDN()); 120 59 121 60 $begin = null; 122 61 $end = null; ··· 138 77 $response->setHTTPResponseCode(206); 139 78 $response->setRange($begin, ($end - 1)); 140 79 } 141 - } else if (isset($validated_token)) { 142 - // We set this on the response, and the response deletes it after the 143 - // transfer completes. This allows transfers to be resumed, in theory. 144 - $response->setTemporaryFileToken($validated_token); 145 80 } 146 81 147 82 $is_viewable = $file->isViewableInBrowser(); ··· 150 85 if ($is_viewable && !$force_download) { 151 86 $response->setMimeType($file->getViewableMimeType()); 152 87 } else { 153 - if (!$request->isHTTPPost() && !$alt_domain) { 88 + if (!$request->isHTTPPost() && !$is_alternate_domain) { 154 89 // NOTE: Require POST to download files from the primary domain. We'd 155 90 // rather go full-bore and do a real CSRF check, but can't currently 156 91 // authenticate users on the file domain. This should blunt any ··· 172 107 $response->setContentIterator($iterator); 173 108 174 109 return $response; 175 - } 176 - 177 - /** 178 - * Add passthrough parameters to the URI so they aren't lost when we 179 - * redirect to acquire tokens. 180 - */ 181 - private function addURIParameters(PhutilURI $uri) { 182 - $request = $this->getRequest(); 183 - 184 - if ($request->getBool('download')) { 185 - $uri->setQueryParam('download', 1); 186 - } 187 - 188 - return $uri; 189 110 } 190 111 191 112 private function loadFile(PhabricatorUser $viewer) {
+2 -50
src/applications/files/storage/PhabricatorFile.php
··· 697 697 pht('You must save a file before you can generate a view URI.')); 698 698 } 699 699 700 - return $this->getCDNURI(null); 700 + return $this->getCDNURI(); 701 701 } 702 702 703 - private function getCDNURI($token) { 703 + public function getCDNURI() { 704 704 $name = self::normalizeFileName($this->getName()); 705 705 $name = phutil_escape_uri($name); 706 706 ··· 720 720 721 721 $parts[] = $this->getSecretKey(); 722 722 $parts[] = $this->getPHID(); 723 - if ($token) { 724 - $parts[] = $token; 725 - } 726 723 $parts[] = $name; 727 724 728 725 $path = '/'.implode('/', $parts); ··· 735 732 } else { 736 733 return PhabricatorEnv::getCDNURI($path); 737 734 } 738 - } 739 - 740 - /** 741 - * Get the CDN URI for this file, including a one-time-use security token. 742 - * 743 - */ 744 - public function getCDNURIWithToken() { 745 - if (!$this->getPHID()) { 746 - throw new Exception( 747 - pht('You must save a file before you can generate a CDN URI.')); 748 - } 749 - 750 - return $this->getCDNURI($this->generateOneTimeToken()); 751 735 } 752 736 753 737 ··· 1119 1103 $this->metadata[self::METADATA_PROFILE] = $value; 1120 1104 return $this; 1121 1105 } 1122 - 1123 - protected function generateOneTimeToken() { 1124 - $key = Filesystem::readRandomCharacters(16); 1125 - $token_type = PhabricatorFileAccessTemporaryTokenType::TOKENTYPE; 1126 - 1127 - // Save the new secret. 1128 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 1129 - $token = id(new PhabricatorAuthTemporaryToken()) 1130 - ->setTokenResource($this->getPHID()) 1131 - ->setTokenType($token_type) 1132 - ->setTokenExpires(time() + phutil_units('1 hour in seconds')) 1133 - ->setTokenCode(PhabricatorHash::digest($key)) 1134 - ->save(); 1135 - unset($unguarded); 1136 - 1137 - return $key; 1138 - } 1139 - 1140 - public function validateOneTimeToken($token_code) { 1141 - $token_type = PhabricatorFileAccessTemporaryTokenType::TOKENTYPE; 1142 - 1143 - $token = id(new PhabricatorAuthTemporaryTokenQuery()) 1144 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 1145 - ->withTokenResources(array($this->getPHID())) 1146 - ->withTokenTypes(array($token_type)) 1147 - ->withExpired(false) 1148 - ->withTokenCodes(array(PhabricatorHash::digest($token_code))) 1149 - ->executeOne(); 1150 - 1151 - return $token; 1152 - } 1153 - 1154 1106 1155 1107 /** 1156 1108 * Write the policy edge between this file and some object.
-17
src/applications/files/temporarytoken/PhabricatorFileAccessTemporaryTokenType.php
··· 1 - <?php 2 - 3 - final class PhabricatorFileAccessTemporaryTokenType 4 - extends PhabricatorAuthTemporaryTokenType { 5 - 6 - const TOKENTYPE = 'file:onetime'; 7 - 8 - public function getTokenTypeDisplayName() { 9 - return pht('File Access'); 10 - } 11 - 12 - public function getTokenReadableTypeName( 13 - PhabricatorAuthTemporaryToken $token) { 14 - return pht('File Access Token'); 15 - } 16 - 17 - }
+2 -1
src/applications/system/controller/PhabricatorRobotsController.php
··· 32 32 33 33 return id(new AphrontPlainTextResponse()) 34 34 ->setContent($content) 35 - ->setCacheDurationInSeconds(phutil_units('2 hours in seconds')); 35 + ->setCacheDurationInSeconds(phutil_units('2 hours in seconds')) 36 + ->setCanCDN(true); 36 37 } 37 38 }