Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
357 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
<?php | ||
|
||
namespace SilverStripe\EnvironmentCheck\Checks; | ||
|
||
use SilverStripe\Control\Director; | ||
use SilverStripe\Control\Controller; | ||
use SilverStripe\ORM\ValidationResult; | ||
use Psr\Http\Message\ResponseInterface; | ||
use SilverStripe\Core\Config\Configurable; | ||
use SilverStripe\EnvironmentCheck\Traits\Fetcher; | ||
use SilverStripe\EnvironmentCheck\EnvironmentCheck; | ||
|
||
/** | ||
* Check cache headers for any response, can specify directives that must be included and | ||
* also must be excluded from Cache-Control headers in response. Also checks for | ||
* existence of ETag. | ||
* | ||
* @example SilverStripe\EnvironmentCheck\Checks\CacheHeadersCheck("/", ["must-revalidate", "max-age=120"], ["no-store"]) | ||
* @package environmentcheck | ||
*/ | ||
class CacheHeadersCheck implements EnvironmentCheck | ||
{ | ||
use Configurable; | ||
use Fetcher; | ||
|
||
/** | ||
* URL to check | ||
* | ||
* @var string | ||
*/ | ||
protected $url; | ||
|
||
/** | ||
* Settings that must be included in the Cache-Control header | ||
* | ||
* @var array | ||
*/ | ||
protected $mustInclude = []; | ||
|
||
/** | ||
* Settings that must be excluded in the Cache-Control header | ||
* | ||
* @var array | ||
*/ | ||
protected $mustExclude = []; | ||
|
||
/** | ||
* Result to keep track of status and messages for all checks, reuses | ||
* ValidationResult for convenience. | ||
* | ||
* @var ValidationResult | ||
*/ | ||
protected $result; | ||
|
||
/** | ||
* Set up with URL, arrays of header settings to check. | ||
* | ||
* @param string $url | ||
* @param array $mustInclude Settings that must be included in Cache-Control | ||
* @param array $mustExclude Settings that must be excluded in Cache-Control | ||
*/ | ||
public function __construct($url = '', $mustInclude = [], $mustExclude = []) | ||
{ | ||
$this->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() | ||
{ | ||
$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) | ||
{ | ||
$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) | ||
{ | ||
$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}" | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<?php | ||
|
||
namespace SilverStripe\EnvironmentCheck\Checks; | ||
|
||
use SilverStripe\Control\Director; | ||
use SilverStripe\Control\Controller; | ||
use Psr\Http\Message\ResponseInterface; | ||
use SilverStripe\Core\Config\Configurable; | ||
use SilverStripe\EnvironmentCheck\Traits\Fetcher; | ||
use SilverStripe\EnvironmentCheck\EnvironmentCheck; | ||
|
||
/** | ||
* Check that a given URL does not generate a session. | ||
* | ||
* @author Adrian Humphreys | ||
* @package environmentcheck | ||
*/ | ||
class SessionCheck implements EnvironmentCheck | ||
{ | ||
use Configurable; | ||
use Fetcher; | ||
|
||
/** | ||
* URL to check | ||
* | ||
* @var string | ||
*/ | ||
protected $url; | ||
|
||
/** | ||
* Set up check with URL | ||
* | ||
* @param string $url The route, excluding the domain | ||
* @inheritdoc | ||
*/ | ||
public function __construct($url = '') | ||
{ | ||
$this->url = $url; | ||
$this->clientConfig = [ | ||
'base_uri' => Director::absoluteBaseURL(), | ||
'timeout' => 10.0, | ||
]; | ||
} | ||
|
||
/** | ||
* Check that the response for URL does not create a session | ||
* | ||
* @return array | ||
*/ | ||
public function check() | ||
{ | ||
$response = $this->fetchResponse($this->url); | ||
$cookie = $this->getCookie($response); | ||
$fullURL = Controller::join_links(Director::absoluteBaseURL(), $this->url); | ||
|
||
if ($cookie) { | ||
return [ | ||
EnvironmentCheck::ERROR, | ||
"Sessions are being set for {$fullURL} : Set-Cookie => " . $cookie, | ||
]; | ||
} | ||
return [ | ||
EnvironmentCheck::OK, | ||
"Sessions are not being created for {$fullURL} 👍", | ||
]; | ||
} | ||
|
||
/** | ||
* Get PHPSESSID or SECSESSID cookie set from the response if it exists. | ||
* | ||
* @param ResponseInterface $response | ||
* @return string|null Cookie contents or null if it doesn't exist | ||
*/ | ||
public function getCookie(ResponseInterface $response) | ||
{ | ||
$result = null; | ||
$cookies = $response->getHeader('Set-Cookie'); | ||
|
||
foreach ($cookies as $cookie) { | ||
if (strpos($cookie, 'SESSID') !== false) { | ||
$result = $cookie; | ||
} | ||
} | ||
return $result; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
|
||
namespace SilverStripe\EnvironmentCheck\Traits; | ||
|
||
use GuzzleHttp\Client; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
/** | ||
* Simple helper for fetching responses using Guzzle client. | ||
* | ||
* @package environmentcheck | ||
*/ | ||
trait Fetcher | ||
{ | ||
/** | ||
* Configuration for the Guzzle client | ||
* | ||
* @var array | ||
*/ | ||
protected $clientConfig = []; | ||
|
||
/** | ||
* Merges configuration arrays and returns the result | ||
* | ||
* @param array $extraConfig | ||
* @return array | ||
*/ | ||
private function getClientConfig(array $extraConfig = []) | ||
{ | ||
return array_merge($this->clientConfig, $extraConfig); | ||
} | ||
|
||
/** | ||
* Fetch a response for a URL using Guzzle client. | ||
* | ||
* @param string $url | ||
* @param array|null $extraConfig Extra configuration | ||
* @return ResponseInterface | ||
*/ | ||
public function fetchResponse(string $url, ? array $extraConfig = []) | ||
{ | ||
$config = $this->getClientConfig($extraConfig); | ||
$client = new Client($config); | ||
return $client->get($url); | ||
} | ||
} |