From c1bfd5640db7e3a8ab08aa0ef9a754757a0c3204 Mon Sep 17 00:00:00 2001 From: Frank Mullenger Date: Fri, 15 Feb 2019 11:57:14 +1300 Subject: [PATCH] NEW: Cache headers check. --- readme.md | 2 + src/Checks/CacheHeadersCheck.php | 220 +++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 src/Checks/CacheHeadersCheck.php diff --git a/readme.md b/readme.md index f0be5cb..b034876 100644 --- a/readme.md +++ b/readme.md @@ -92,6 +92,8 @@ SilverStripe\EnvironmentCheck\EnvironmentCheckSuite: * `SMTPConnectCheck`: Checks if the SMTP connection configured through PHP.ini works as expected. * `SolrIndexCheck`: Checks if the Solr cores of given class are available. * `SessionCheck`: Checks that a given URL does not generate a session. + * `CacheHeadersCheck`: Check cache headers in response for directives that must either be included or excluded as well + checking for existence of ETag. ## Monitoring Checks diff --git a/src/Checks/CacheHeadersCheck.php b/src/Checks/CacheHeadersCheck.php new file mode 100644 index 0000000..a70effc --- /dev/null +++ b/src/Checks/CacheHeadersCheck.php @@ -0,0 +1,220 @@ +url = $url; + $this->mustInclude = $mustInclude; + $this->mustExclude = $mustExclude; + + $this->clientConfig = [ + 'base_uri' => Director::absoluteBaseURL(), + 'timeout' => 10.0, + ]; + + // Using a validation result to capture messages + $this->result = new ValidationResult(); + } + + /** + * Check that correct caching headers are present. + * + * @return void + */ + public function check() + { + $response = $this->fetchResponse($this->url); + $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url); + if ($response === null) { + return [ + EnvironmentCheck::ERROR, + "Cache headers check request failed for $fullURL", + ]; + } + + //Check that Etag exists + $this->checkEtag($response); + + // Check Cache-Control settings + $this->checkCacheControl($response); + + if ($this->result->isValid()) { + return [ + EnvironmentCheck::OK, + $this->getMessage(), + ]; + } else { + // @todo Ability to return a warning + return [ + EnvironmentCheck::ERROR, + $this->getMessage(), + ]; + } + } + + /** + * Collate messages from ValidationResult so that it is clear which parts + * of the check passed and which failed. + * + * @return string + */ + private function getMessage(): string + { + $ret = ''; + // Filter good messages + $goodTypes = [ValidationResult::TYPE_GOOD, ValidationResult::TYPE_INFO]; + $good = array_filter( + $this->result->getMessages(), + function ($val, $key) use ($goodTypes) { + if (in_array($val['messageType'], $goodTypes)) { + return true; + } + return false; + }, + ARRAY_FILTER_USE_BOTH + ); + if (!empty($good)) { + $ret .= "GOOD: " . implode('; ', array_column($good, 'message')) . " "; + } + + // Filter bad messages + $badTypes = [ValidationResult::TYPE_ERROR, ValidationResult::TYPE_WARNING]; + $bad = array_filter( + $this->result->getMessages(), + function ($val, $key) use ($badTypes) { + if (in_array($val['messageType'], $badTypes)) { + return true; + } + return false; + }, + ARRAY_FILTER_USE_BOTH + ); + if (!empty($bad)) { + $ret .= "BAD: " . implode('; ', array_column($bad, 'message')); + } + return $ret; + } + + /** + * Check that ETag header exists + * + * @param ResponseInterface $response + * @return void + */ + private function checkEtag(ResponseInterface $response): void + { + $eTag = $response->getHeaderLine('ETag'); + $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url); + + if ($eTag) { + $this->result->addMessage( + "$fullURL includes an Etag header in response", + ValidationResult::TYPE_GOOD + ); + return; + } + $this->result->addError( + "$fullURL is missing an Etag header", + ValidationResult::TYPE_WARNING + ); + } + + /** + * Check that the correct header settings are either included or excluded. + * + * @param ResponseInterface $response + * @return void + */ + private function checkCacheControl(ResponseInterface $response): void + { + $cacheControl = $response->getHeaderLine('Cache-Control'); + $vals = array_map('trim', explode(',', $cacheControl)); + $fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url); + + // All entries from must contain should be present + if ($this->mustInclude == array_intersect($this->mustInclude, $vals)) { + $matched = implode(",", $this->mustInclude); + $this->result->addMessage( + "$fullURL includes all settings: {$matched}", + ValidationResult::TYPE_GOOD + ); + } else { + $missing = implode(",", array_diff($this->mustInclude, $vals)); + $this->result->addError( + "$fullURL is excluding some settings: {$missing}" + ); + } + + // All entries from must exclude should not be present + if (empty(array_intersect($this->mustExclude, $vals))) { + $missing = implode(",", $this->mustExclude); + $this->result->addMessage( + "$fullURL excludes all settings: {$missing}", + ValidationResult::TYPE_GOOD + ); + } else { + $matched = implode(",", array_intersect($this->mustExclude, $vals)); + $this->result->addError( + "$fullURL is including some settings: {$matched}" + ); + } + } +}