From 39388edfe487e17e21416a94a262daf8c476c743 Mon Sep 17 00:00:00 2001 From: Stef van den Ham Date: Fri, 21 May 2021 14:56:15 +0200 Subject: [PATCH 1/5] Adds support for Akamai purging --- README.md | 12 +++ composer.json | 6 +- src/config.example.php | 11 ++ src/drivers/Akamai.php | 144 ++++++++++++++++++++++++++ src/exceptions/AkamaiApiException.php | 44 ++++++++ 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/drivers/Akamai.php create mode 100644 src/exceptions/AkamaiApiException.php diff --git a/README.md b/README.md index b8ad305..83ea173 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ If you are looking additional integrations (Cache Drivers), feel free to contrib * [KeyCDN](https://www.keycdn.com) (CDN/SaaS) * [Fastly](https://www.fastly.com) (CDN/SaaS) * [Cloudflare](https://www.cloudflare.com) (CDN/SaaS) +* [Akamai](https://www.akamai.com) (CDN/SaaS) * Varnish with XKEY support (your own proxy) * Dummy (does nothing) @@ -80,6 +81,17 @@ UPPER_DRIVER=varnish VARNISH_URL= ``` +### Akamai Setup + +``` +UPPER_DRIVER=akamai +AKAMAI_HOST= +AKAMAI_CLIENT_TOKEN= +AKAMAI_CLIENT_SECRET= +AKAMAI_ACCESS_TOKEN= +AKAMAI_MAX_SIZE=2048 +``` + ### Tuning With `Cache-Control` headers you can disabled caching for certain templates: diff --git a/composer.json b/composer.json index 913f2a1..228b260 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "cache", "cloudflare", "fastly", - "keycdn" + "keycdn", + "akamai" ], "support": { "docs": "https://github.com/ostark/upper/blob/master/README.md", @@ -25,7 +26,8 @@ ], "require": { "craftcms/cms": "^3.2.0", - "guzzlehttp/guzzle": "^6.5.5|^7.2.0" + "guzzlehttp/guzzle": "^6.5.5|^7.2.0", + "akamai-open/edgegrid-auth": "^1.0" }, "require-dev": { "vimeo/psalm": "^4.4" diff --git a/src/config.example.php b/src/config.example.php index c1724e2..a4e51f4 100644 --- a/src/config.example.php +++ b/src/config.example.php @@ -1,4 +1,5 @@ getenv('CLOUDFLARE_API_EMAIL'), ], + // Akamai config + 'akamai' => [ + 'tagHeaderName' => 'Edge-Cache-Tag', + 'host' => getenv('AKAMAI_HOST'), + 'clientToken' => getenv('AKAMAI_CLIENT_TOKEN'), + 'clientSecret' => getenv('AKAMAI_CLIENT_SECRET'), + 'accessToken' => getenv('AKAMAI_ACCESS_TOKEN'), + 'maxSize' => getenv('AKAMAI_MAX_SIZE'), + ], + // Dummy driver (default) 'dummy' => [ 'tagHeaderName' => 'X-CacheTag', diff --git a/src/drivers/Akamai.php b/src/drivers/Akamai.php new file mode 100644 index 0000000..215a9e7 --- /dev/null +++ b/src/drivers/Akamai.php @@ -0,0 +1,144 @@ +useLocalTags) { + return $this->purgeUrlsByTag($tag); + } + + return $this->sendRequest('POST', 'tag', $tag); + } + + /** + * Purge cache by urls + * + * @param array $urls + * + * @return bool + */ + public function purgeUrls(array $urls) + { + foreach ($urls as $url) { + if (!$this->sendRequest('POST', 'url', $url)) { + return false; + } + } + + return true; + } + + + /** + * Purge entire cache + * + * @return bool + */ + public function purgeAll() + { + // TODO: Purge all in Akamai + return true; + // return $this->sendRequest('POST', 'purge_all'); + } + + /** + * Send API call + * + * @param string $method HTTP verb + * @param string $type of purge + * @param string $uri + * @param array $headers + * + * @return bool + * @throws \ostark\upper\exceptions\AkamaiApiException + */ + protected function sendRequest(string $method = 'POST', string $type = "url", string $uri = "", array $headers = []) + { + // Akamai Open Edgegrid reads $_ENV which doesn't get populated by Craft, so filling in the blanks + $_ENV['AKAMAI_HOST'] = getenv('AKAMAI_HOST'); + $_ENV['AKAMAI_CLIENT_TOKEN'] = getenv('AKAMAI_CLIENT_TOKEN'); + $_ENV['AKAMAI_CLIENT_SECRET'] = getenv('AKAMAI_CLIENT_SECRET'); + $_ENV['AKAMAI_ACCESS_TOKEN'] = getenv('AKAMAI_ACCESS_TOKEN'); + $_ENV['AKAMAI_MAX_SIZE'] = getenv('AKAMAI_MAX_SIZE'); + + $auth = \Akamai\Open\EdgeGrid\Authentication::createFromEnv(); + + $auth->setHttpMethod('POST'); + $auth->setPath('/ccu/v3/invalidate/' . $type . '/production'); + + $body = json_encode(array( + 'objects' => array(getenv('DEFAULT_SITE_URL') . $uri) + )); + + $auth->setBody($body); + + $context = array( + 'http' => array( + 'header' => array( + 'Authorization: ' . $auth->createAuthHeader(), + 'Content-Type: application/json', + 'Content-Length: ' . strlen($body), + ), + 'method' => 'POST', + 'content' => $body + ) + ); + + $context = stream_context_create($context); + + try { + json_decode(file_get_contents('https://' . $auth->getHost() . $auth->getPath(), false, $context)); + } catch (BadResponseException $e) { + throw AkamaiApiException::create( + $e->getRequest(), + $e->getResponse() + ); + } + + return true; + } +} diff --git a/src/exceptions/AkamaiApiException.php b/src/exceptions/AkamaiApiException.php new file mode 100644 index 0000000..d49023b --- /dev/null +++ b/src/exceptions/AkamaiApiException.php @@ -0,0 +1,44 @@ +getUri(); + + if (is_null($response)) { + return new static("Akamai no response error, uri: '$uri'"); + } + + // Extract error message from body + $status = $response->getStatusCode(); + $json = json_decode($response->getBody()); + if (json_last_error() !== JSON_ERROR_NONE) { + return new static("Akamai API error ($status) on: '$uri'", $status); + } + + // Error message + if (isset($json->msg)) { + return new static($json->msg . ", uri: '$uri'", $response->getStatusCode()); + } + + // Unknown + return new static("Unknown error, uri: '$uri'"); + } +} From c61c3d786d70d673a8688a6f0b1ce152daef2335 Mon Sep 17 00:00:00 2001 From: Hyra Date: Thu, 12 Aug 2021 14:44:43 +0200 Subject: [PATCH 2/5] Also purge staging environment --- src/drivers/Akamai.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/drivers/Akamai.php b/src/drivers/Akamai.php index 215a9e7..fb16de7 100644 --- a/src/drivers/Akamai.php +++ b/src/drivers/Akamai.php @@ -51,7 +51,8 @@ public function purgeTag(string $tag) return $this->purgeUrlsByTag($tag); } - return $this->sendRequest('POST', 'tag', $tag); + return $this->sendRequest('production', 'POST', 'tag', $tag); + return $this->sendRequest('staging', 'POST', 'tag', $tag); } /** @@ -64,7 +65,10 @@ public function purgeTag(string $tag) public function purgeUrls(array $urls) { foreach ($urls as $url) { - if (!$this->sendRequest('POST', 'url', $url)) { + if (!$this->sendRequest('production', 'POST', 'url', $url)) { + return false; + } + if (!$this->sendRequest('staging', 'POST', 'url', $url)) { return false; } } @@ -96,7 +100,7 @@ public function purgeAll() * @return bool * @throws \ostark\upper\exceptions\AkamaiApiException */ - protected function sendRequest(string $method = 'POST', string $type = "url", string $uri = "", array $headers = []) + protected function sendRequest(string $environment = 'production', string $method = 'POST', string $type = "url", string $uri = "", array $headers = []) { // Akamai Open Edgegrid reads $_ENV which doesn't get populated by Craft, so filling in the blanks $_ENV['AKAMAI_HOST'] = getenv('AKAMAI_HOST'); @@ -108,7 +112,7 @@ protected function sendRequest(string $method = 'POST', string $type = "url", st $auth = \Akamai\Open\EdgeGrid\Authentication::createFromEnv(); $auth->setHttpMethod('POST'); - $auth->setPath('/ccu/v3/invalidate/' . $type . '/production'); + $auth->setPath('/ccu/v3/invalidate/' . $type . '/' . $environment); $body = json_encode(array( 'objects' => array(getenv('DEFAULT_SITE_URL') . $uri) From 958c75905b45c5cda5d837108ceae1fe131395d4 Mon Sep 17 00:00:00 2001 From: Hyra Date: Thu, 12 Aug 2021 15:58:20 +0200 Subject: [PATCH 3/5] Job ttr of 1200 --- src/EventRegistrar.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/EventRegistrar.php b/src/EventRegistrar.php index a83dc57..0909ec1 100644 --- a/src/EventRegistrar.php +++ b/src/EventRegistrar.php @@ -1,4 +1,6 @@ -getRequest(); // Don't cache CP, LivePreview, Action, Non-GET requests - if ($request->getIsCpRequest() || + if ( + $request->getIsCpRequest() || $request->getIsLivePreview() || $request->getIsActionRequest() || !$request->getIsGet() @@ -87,7 +89,6 @@ public static function registerFrontendEvents() // Add to collection Plugin::getInstance()->getTagCollection()->addTagsFromElement($event->row); - }); // Add the tags to the response header @@ -118,7 +119,8 @@ public static function registerFrontendEvents() $response->setSharedMaxAge($maxAge); $headers->set(Plugin::INFO_HEADER_NAME, "CACHED: " . date(\DateTime::ISO8601)); - $plugin->trigger($plugin::EVENT_AFTER_SET_TAG_HEADER, new CacheResponseEvent([ + $plugin->trigger($plugin::EVENT_AFTER_SET_TAG_HEADER, new CacheResponseEvent( + [ 'tags' => $tags, 'maxAge' => $maxAge, 'requestUrl' => \Craft::$app->getRequest()->getUrl(), @@ -126,7 +128,6 @@ public static function registerFrontendEvents() ] )); }); - } @@ -172,7 +173,7 @@ public static function registerFallback() // Insert item \Craft::$app->getDb()->createCommand() ->upsert( - // Table + // Table Plugin::CACHE_TABLE, // Identifier @@ -191,9 +192,7 @@ public static function registerFallback() } catch (\Exception $e) { \Craft::warning("Failed to register fallback.", "upper"); } - }); - } @@ -257,14 +256,13 @@ protected static function handleUpdateEvent(Event $event) Plugin::getInstance()->trigger(Plugin::EVENT_BEFORE_PURGE, $purgeEvent); // Push to queue - \Craft::$app->getQueue()->push(new PurgeCacheJob([ + \Craft::$app->getQueue()->ttr(1200)->push(new PurgeCacheJob( + [ 'tag' => $purgeEvent->tag ] )); Plugin::getInstance()->trigger(Plugin::EVENT_AFTER_PURGE, $purgeEvent); } - } - } From f179a7c168a02aebfa3b74fbe39e7b85b34f419c Mon Sep 17 00:00:00 2001 From: Hyra Date: Mon, 16 Aug 2021 16:22:11 +0200 Subject: [PATCH 4/5] Only use URL when purging URLs --- src/drivers/Akamai.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/drivers/Akamai.php b/src/drivers/Akamai.php index fb16de7..08e05fa 100644 --- a/src/drivers/Akamai.php +++ b/src/drivers/Akamai.php @@ -65,10 +65,10 @@ public function purgeTag(string $tag) public function purgeUrls(array $urls) { foreach ($urls as $url) { - if (!$this->sendRequest('production', 'POST', 'url', $url)) { + if (!$this->sendRequest('production', 'POST', 'url', getenv('DEFAULT_SITE_URL') . $url)) { return false; } - if (!$this->sendRequest('staging', 'POST', 'url', $url)) { + if (!$this->sendRequest('staging', 'POST', 'url', getenv('DEFAULT_SITE_URL') . $url)) { return false; } } @@ -115,7 +115,7 @@ protected function sendRequest(string $environment = 'production', string $metho $auth->setPath('/ccu/v3/invalidate/' . $type . '/' . $environment); $body = json_encode(array( - 'objects' => array(getenv('DEFAULT_SITE_URL') . $uri) + 'objects' => array($uri) )); $auth->setBody($body); From 34010a74842f5d9080631b5fe9f9d67b40f2dea3 Mon Sep 17 00:00:00 2001 From: Hyra Date: Wed, 18 Aug 2021 09:16:42 +0200 Subject: [PATCH 5/5] Return later --- src/drivers/Akamai.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/drivers/Akamai.php b/src/drivers/Akamai.php index 08e05fa..6fbcf11 100644 --- a/src/drivers/Akamai.php +++ b/src/drivers/Akamai.php @@ -51,8 +51,10 @@ public function purgeTag(string $tag) return $this->purgeUrlsByTag($tag); } - return $this->sendRequest('production', 'POST', 'tag', $tag); - return $this->sendRequest('staging', 'POST', 'tag', $tag); + $this->sendRequest('production', 'POST', 'tag', $tag); + $this->sendRequest('staging', 'POST', 'tag', $tag); + + return true; } /**