From 8543f5af4671d7e0c597cad7feec8122419770d4 Mon Sep 17 00:00:00 2001 From: Tom Davies Date: Thu, 9 Apr 2020 20:18:37 +0100 Subject: [PATCH] Update Cloudflare driver to implement scoped API tokens (#39) * Update Cloudflare driver to support scoped CF api tokens, deprecate using account-wide CF api keys * Attach custom behaviours to Response before registering front end events to prevent error * review fixes --- README.md | 5 +++-- src/Plugin.php | 9 ++++---- src/config.example.php | 6 +++-- src/drivers/Cloudflare.php | 45 ++++++++++++++++++++++++++------------ 4 files changed, 42 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a9b6f50..5d4163e 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,7 @@ KEYCDN_ZONE_ID= ``` UPPER_DRIVER=cloudflare -CLOUDFLARE_API_KEY= -CLOUDFLARE_API_EMAIL= +CLOUDFLARE_API_TOKEN= CLOUDFLARE_ZONE_ID= CLOUDFLARE_DOMAIN=https:// ``` @@ -71,6 +70,8 @@ By default, Cloudflare's CDN does not cache HTML content. You need to create a If you don't use Cloudflare Enterprise with native `Cache-Tag` support, make sure to enable `useLocalTags` in your `config/upper.php` file (default), otherwise disable it. +You can generate a token in the Cloudflare dashboard. You want to create a custom token with the "Zone.Cache Purge" permission that is restricted to the DNS zone(s) you wish to clear Cloudflare's cache for. + ### Varnish Setup Varnish URL supports multiple servers, separate with comma. E.g `http://1.1.1.1,http://2.2.2.2` diff --git a/src/Plugin.php b/src/Plugin.php index 32c4b89..07739be 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -61,6 +61,10 @@ public function init() 'tagCollection' => TagCollection::class ]); + // Attach Behaviors + \Craft::$app->getResponse()->attachBehavior('cache-control', CacheControlBehavior::class); + \Craft::$app->getResponse()->attachBehavior('tag-header', TagHeaderBehavior::class); + // Register event handlers EventRegistrar::registerFrontendEvents(); EventRegistrar::registerCpEvents(); @@ -69,11 +73,6 @@ public function init() if ($this->getSettings()->useLocalTags) { EventRegistrar::registerFallback(); } - - // Attach Behaviors - \Craft::$app->getResponse()->attachBehavior('cache-control', CacheControlBehavior::class); - \Craft::$app->getResponse()->attachBehavior('tag-header', TagHeaderBehavior::class); - } // ServiceLocators diff --git a/src/config.example.php b/src/config.example.php index e8b5f6c..393f5a7 100644 --- a/src/config.example.php +++ b/src/config.example.php @@ -53,10 +53,12 @@ 'cloudflare' => [ 'tagHeaderName' => 'Cache-Tag', 'tagHeaderDelimiter' => ',', + 'apiToken' => getenv('CLOUDFLARE_API_TOKEN'), + 'zoneId' => getenv('CLOUDFLARE_ZONE_ID'), + 'domain' => getenv('CLOUDFLARE_DOMAIN'), + // deprecated, do not use for new installs 'apiKey' => getenv('CLOUDFLARE_API_KEY'), 'apiEmail' => getenv('CLOUDFLARE_API_EMAIL'), - 'zoneId' => getenv('CLOUDFLARE_ZONE_ID'), - 'domain' => getenv('CLOUDFLARE_DOMAIN') ], // Dummy driver (default) diff --git a/src/drivers/Cloudflare.php b/src/drivers/Cloudflare.php index 9d51653..6e5ab33 100644 --- a/src/drivers/Cloudflare.php +++ b/src/drivers/Cloudflare.php @@ -1,5 +1,6 @@ useLocalTags) { - return $this->purgeUrlsByTag($tag); + return $this->purgeUrlsByTag($tag); } return $this->sendRequest('DELETE', 'purge_cache', [ @@ -57,12 +60,12 @@ public function purgeUrls(array $urls) } // prefix urls with domain - $files = array_map(function ($url) { + $files = array_map(function($url) { return rtrim($this->domain, '/') . $url; }, $urls); // Chunk larger collections to meet the API constraints - foreach(array_chunk($files, self::MAX_URLS_PER_PURGE) as $fileGroup) { + foreach (array_chunk($files, self::MAX_URLS_PER_PURGE) as $fileGroup) { $this->sendRequest('DELETE', 'purge_cache', [ 'files' => $fileGroup ]); @@ -100,21 +103,12 @@ public function purgeAll() */ protected function sendRequest($method = 'DELETE', string $type, array $params = []) { - $client = new Client([ - 'base_uri' => self::API_ENDPOINT, - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Auth-Key' => $this->apiKey, - 'X-Auth-Email' => $this->apiEmail, - ] - ]); + $client = $this->getClient(); try { - - $uri = "zones/{$this->zoneId}/$type"; + $uri = "zones/{$this->zoneId}/$type"; $options = (count($params)) ? ['json' => $params] : []; $client->request($method, $uri, $options); - } catch (BadResponseException $e) { throw CloudflareApiException::create( @@ -126,6 +120,29 @@ protected function sendRequest($method = 'DELETE', string $type, array $params = return true; } + private function getClient() + { + $headers = [ + 'Content-Type' => 'application/json', + ]; + if ($this->usesLegacyApiKey()) { + Craft::$app->getDeprecator()->log('Upper Config: Cloudflare $apiKey', 'Globally scoped Cloudflare API keys are deprecated for security. Create a scoped token instead and use via the `apiToken` key in the driver config.'); + $headers['X-Auth-Key'] = $this->apiKey; + $headers['X-Auth-Email'] = $this->apiEmail; + } else { + $headers['Authorization'] = 'Bearer ' . $this->apiToken; + } + + return new Client([ + 'base_uri' => self::API_ENDPOINT, + 'headers' => $headers, + ]); + } + + private function usesLegacyApiKey() + { + return !isset($this->apiToken) && isset($this->apiKey); + } }