-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #61 from paragonie/short-lived-cache
Optional feature: Cache responses for a very short period of time.
- Loading branch information
Showing
10 changed files
with
375 additions
and
4 deletions.
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
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,12 @@ | ||
<?php | ||
declare(strict_types=1); | ||
namespace ParagonIE\Chronicle\Exception; | ||
|
||
/** | ||
* Class CacheMisuseException | ||
* @package ParagonIE\Chronicle\Exception | ||
*/ | ||
class CacheMisuseException extends BaseException | ||
{ | ||
|
||
} |
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,192 @@ | ||
<?php | ||
declare(strict_types=1); | ||
namespace ParagonIE\Chronicle; | ||
|
||
use Cache\Adapter\Memcached\MemcachedCachePool; | ||
use ParagonIE\Chronicle\Exception\CacheMisuseException; | ||
use ParagonIE\ConstantTime\Base32; | ||
use Psr\Cache\CacheItemInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\StreamInterface; | ||
use Slim\Http\Headers; | ||
use Slim\Http\Response; | ||
use Slim\Http\Stream; | ||
|
||
/** | ||
* Class ResponseCache | ||
* @package ParagonIE\Chronicle | ||
*/ | ||
class ResponseCache | ||
{ | ||
/** @var string $cacheKey */ | ||
private $cacheKey = ''; | ||
|
||
/** @var int|null $lifetime */ | ||
private $lifetime; | ||
|
||
/** @var MemcachedCachePool $memcached */ | ||
private $memcached; | ||
|
||
/** | ||
* ResponseCache constructor. | ||
* @param int $lifetime | ||
* @throws \Psr\Cache\InvalidArgumentException | ||
* @throws CacheMisuseException | ||
*/ | ||
public function __construct(int $lifetime = 0) | ||
{ | ||
if (!self::isAvailable()) { | ||
throw new CacheMisuseException('Memcached is not installed.'); | ||
} | ||
$client = new \Memcached(); | ||
$client->addServer('localhost', 11211); | ||
if ($lifetime > 0) { | ||
$this->lifetime = $lifetime; | ||
} | ||
$this->memcached = new MemcachedCachePool($client); | ||
$this->loadCacheKey(); | ||
} | ||
|
||
/** | ||
* @return string | ||
* @throws \Psr\Cache\InvalidArgumentException | ||
* @throws CacheMisuseException | ||
*/ | ||
public function loadCacheKey() | ||
{ | ||
if (!empty($this->cacheKey)) { | ||
return $this->cacheKey; | ||
} | ||
if ($this->memcached->hasItem('ChronicleCacheKey')) { | ||
/** @var CacheItemInterface $item */ | ||
$item = $this->memcached->getItem('ChronicleCacheKey'); | ||
return (string) $item->get(); | ||
} | ||
try { | ||
$key = sodium_crypto_shorthash_keygen(); | ||
} catch (\Throwable $ex) { | ||
throw new CacheMisuseException('CSPRNG failure', 0, $ex); | ||
} | ||
/** @var CacheItemInterface $item */ | ||
$item = $this->memcached->getItem('ChronicleCacheKey'); | ||
$item->set($key); | ||
$item->expiresAfter(null); | ||
$this->memcached->save($item); | ||
return $key; | ||
} | ||
|
||
/** | ||
* @return bool | ||
*/ | ||
public static function isAvailable(): bool | ||
{ | ||
return extension_loaded('memcached') && class_exists('Memcached'); | ||
} | ||
|
||
/** | ||
* @param string $input | ||
* @return string | ||
* @throws CacheMisuseException | ||
* @throws \Psr\Cache\InvalidArgumentException | ||
* @throws \SodiumException | ||
*/ | ||
public function getCacheKey(string $input): string | ||
{ | ||
return 'Chronicle|' . Base32::encodeUnpadded( | ||
sodium_crypto_shorthash($input, $this->loadCacheKey()) | ||
); | ||
} | ||
|
||
/** | ||
* @param string $uri | ||
* @return Response|null | ||
* @throws CacheMisuseException | ||
* @throws \Psr\Cache\InvalidArgumentException | ||
* @throws \SodiumException | ||
*/ | ||
public function loadResponse(string $uri) | ||
{ | ||
$key = $this->getCacheKey($uri); | ||
if (!$this->memcached->hasItem($key)) { | ||
return null; | ||
} | ||
/** @var CacheItemInterface $item */ | ||
$item = $this->memcached->getItem($key); | ||
/** @var string|null $cached */ | ||
$cached = $item->get(); | ||
if (!is_string($cached)) { | ||
return null; | ||
} | ||
return $this->deserializeResponse($cached); | ||
} | ||
|
||
/** | ||
* @param string $uri | ||
* @param ResponseInterface $response | ||
* @return void | ||
* @throws CacheMisuseException | ||
* @throws \Psr\Cache\InvalidArgumentException | ||
* @throws \SodiumException | ||
*/ | ||
public function saveResponse(string $uri, ResponseInterface $response) | ||
{ | ||
$key = $this->getCacheKey($uri); | ||
/** @var CacheItemInterface $item */ | ||
$item = $this->memcached->getItem($key); | ||
$item->set($this->serializeResponse($response)); | ||
$item->expiresAfter($this->lifetime); | ||
$this->memcached->save($item); | ||
} | ||
|
||
/** | ||
* @param string $serialized | ||
* @return Response | ||
*/ | ||
public function deserializeResponse(string $serialized): Response | ||
{ | ||
/** @var array<string, string|array|int> $decoded */ | ||
$decoded = json_decode($serialized, true); | ||
$status = (int) $decoded['status']; | ||
$headers = (array) $decoded['headers']; | ||
/** @var string $body */ | ||
$body = $decoded['body']; | ||
|
||
return new Response( | ||
$status, | ||
new Headers($headers), | ||
self::fromString($body) | ||
); | ||
} | ||
|
||
/** | ||
* Create a Stream object from a string. | ||
* | ||
* @param string $input | ||
* @return StreamInterface | ||
* @throws \Error | ||
*/ | ||
public static function fromString(string $input): StreamInterface | ||
{ | ||
/** @var resource $stream */ | ||
$stream = \fopen('php://temp', 'w+'); | ||
if (!\is_resource($stream)) { | ||
throw new \Error('Could not create stream'); | ||
} | ||
\fwrite($stream, $input); | ||
\rewind($stream); | ||
return new Stream($stream); | ||
} | ||
|
||
/** | ||
* @param ResponseInterface $response | ||
* @return string | ||
*/ | ||
public function serializeResponse(ResponseInterface $response): string | ||
{ | ||
return json_encode([ | ||
'status' => $response->getStatusCode(), | ||
'headers' => $response->getHeaders(), | ||
'body' => $response->getBody()->getContents() | ||
]); | ||
} | ||
} |
Oops, something went wrong.