Commit d89dfb02 authored by Cédric Anne's avatar Cédric Anne Committed by Johan Cwiklinski

Check cache integrity across execution contexts

(#5465 adapted to master)
parent 512895a8
......@@ -5,6 +5,7 @@
/config/parameters.yaml
/tests/config_db*
/tests/db.yaml
/tests/parameters.yaml
/plugins/
/files/
/.buildpath
......
......@@ -1516,7 +1516,7 @@ class Config extends CommonDBTM {
function showPerformanceInformations() {
global $CONTAINER;
$cache_storage = $CONTAINER->get('application_cache_storage');
$cache_storage = $CONTAINER->get('application_cache')->getStorage();
if (!Config::canUpdate()) {
return false;
......
......@@ -725,7 +725,7 @@ class Html {
static function displayDebugInfos($with_session = true, $ajax = false) {
global $CFG_GLPI, $CONTAINER, $DEBUG_SQL, $SQL_TOTAL_REQUEST, $SQL_TOTAL_TIMER, $DEBUG_AUTOLOAD;
$cache_storage = $CONTAINER->get('application_cache_storage');
$cache_storage = $CONTAINER->get('application_cache')->getStorage();
// Only for debug mode so not need to be translated
if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) { // mode debug
......
......@@ -624,7 +624,7 @@ class Session {
$_SESSION['glpipluralnumber'] = $CFG_GLPI["languages"][$trytoload][5];
}
$TRANSLATE = new Zend\I18n\Translator\Translator;
$cache_storage = $CONTAINER->get('translation_cache_storage');
$cache_storage = $CONTAINER->get('translation_cache')->getStorage();
$TRANSLATE->setCache($cache_storage);
$TRANSLATE->addTranslationFile('gettext', GLPI_ROOT.$newfile, 'glpi', $trytoload);
......
......@@ -56,8 +56,8 @@ class CacheStorageFactory extends StorageFactory
private static $cacheUniqId;
/**
* @param string $cacheDir Cache directory.
* @param string $cacheUniqId Cache unique identifier.
* @param string $cacheDir Cache directory.
* @param string $cacheUniqId Cache unique identifier.
*/
public function __construct(string $cacheDir, string $cacheUniqId)
{
......@@ -67,36 +67,19 @@ class CacheStorageFactory extends StorageFactory
public static function factory($cfg)
{
// Compute prefered adapter if 'auto' value or no value is used
if (!isset($cfg['adapter']) || 'auto' === $cfg['adapter']) {
if (function_exists('wincache_ucache_add')) {
$cfg['adapter'] = 'wincache';
} elseif (function_exists('apcu_fetch')) {
$cfg['adapter'] = 'apcu';
} else {
$cfg['adapter'] = 'filesystem';
if (!array_key_exists('plugins', $cfg)) {
$cfg['plugins'] = [
'serializer'
];
} elseif (!in_array('serializer', $cfg['plugins'])) {
$cfg['plugins'][] = 'serializer';
}
}
}
// Add unique id to namespace
if (!array_key_exists('options', $cfg) || !is_array($cfg['options'])) {
$cfg['options'] = [];
}
$baseNamespace = isset($cfg['options']['namespace']) ? $cfg['options']['namespace'] : '_default';
$cfg['options']['namespace'] = $baseNamespace . (empty(self::$cacheUniqId) ? '' : '_' . self::$cacheUniqId);
// Add unique id to namespace
$namespace = isset($cfg['options']['namespace']) ? $cfg['options']['namespace'] : '_default';
$namespace .= (empty(self::$cacheUniqId) ? '' : '_' . self::$cacheUniqId);
$cfg['options']['namespace'] = $namespace;
// Handle pathname for dba adapter
if ('dba' === $cfg['adapter']) {
// Assign default value for pathname
if (!isset($cfg['options']['pathname'])) {
$namespace = $cfg['options']['namespace'];
$cfg['options']['pathname'] = self::$cacheDir . '/' . $namespace . '.data';
}
}
......@@ -105,7 +88,6 @@ class CacheStorageFactory extends StorageFactory
if ('filesystem' === $cfg['adapter']) {
// Assign default value for cache dir
if (!isset($cfg['options']['cache_dir'])) {
$namespace = $cfg['options']['namespace'];
$cfg['options']['cache_dir'] = self::$cacheDir . '/' . $namespace;
}
......@@ -130,44 +112,6 @@ class CacheStorageFactory extends StorageFactory
}
}
try {
return parent::factory($cfg);
} catch (\Exception $e) {
if ('filesystem' !== $cfg['adapter']) {
// Fallback to 'filesystem' adapter
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "filesystem" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
return self::factory(
[
'adapter' => 'filesystem',
'options' => [
'namespace' => $baseNamespace . '_fallback'
],
]
);
} else {
// Fallback to 'memory' adapter
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "memory" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
return self::factory(
[
'adapter' => 'memory',
'options' => [
'namespace' => $baseNamespace . '_fallback'
],
]
);
}
}
return parent::factory($cfg);
}
}
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Cache;
if (! defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Zend\Cache\Psr\SimpleCache\SimpleCacheDecorator;
use Zend\Cache\Storage\StorageInterface;
class SimpleCache extends SimpleCacheDecorator
{
/**
* Determines if footprints must be checked.
*
* @var boolean
*/
private $check_footprints;
/**
* Footprint file path, if existing.
*
* @var string|null
*/
private $footprint_file;
/**
* Footprint fallback storage used if footprint file is not available.
*
* @var array
*/
private $footprint_fallback_storage = [];
/**
*
* @var StorageInterface
*/
private $storage;
public function __construct(StorageInterface $storage, $cache_dir, $check_footprints = true)
{
parent::__construct($storage);
$this->storage = $storage;
$this->check_footprints = $check_footprints;
if ($this->check_footprints) {
$this->footprint_file = $cache_dir . '/' . $storage->getOptions()->getNamespace() . '.json';
$this->checkFootprintFileIntegrity();
}
}
public function get($key, $default = null)
{
$cached_value = parent::get($key, $default);
if (! $this->check_footprints) {
return $cached_value;
}
if ($this->getCachedFootprint($key) !== $this->computeFootprint($cached_value)) {
// If footprint changed, value is no more valid.
return $default;
}
return $cached_value;
}
public function set($key, $value, $ttl = null)
{
if ($this->check_footprints) {
$this->setFootprint($key, $value);
}
return parent::set($key, $value, $ttl);
}
public function delete($key)
{
if ($this->check_footprints) {
$this->setFootprint($key, null);
}
return parent::delete($key);
}
public function clear()
{
if ($this->check_footprints) {
$this->setAllCachedFootprints([]);
}
return parent::clear();
}
public function getMultiple($keys, $default = null)
{
$cached_values = parent::getMultiple($keys, $default);
if ($this->check_footprints) {
foreach ($cached_values as $key => $cached_value) {
if ($this->getCachedFootprint($key) !== $this->computeFootprint($cached_value)) {
// If footprint changed, value is no more valid.
$cached_values[$key] = $default;
}
}
}
return $cached_values;
}
public function setMultiple($values, $ttl = null)
{
if ($this->check_footprints) {
$this->setMultipleFootprints($values);
}
return parent::setMultiple($values, $ttl);
}
public function deleteMultiple($keys)
{
if ($this->check_footprints) {
$values = array_combine($keys, array_fill(0, count($keys), null));
$this->setMultipleFootprints($values);
}
return parent::deleteMultiple($keys);
}
public function has($key)
{
if (! parent::has($key)) {
return false;
}
if (! $this->check_footprints) {
return true;
}
// Cache value is not usable if stale, consider it has not existing.
return $this->getCachedFootprint($key) === $this->computeFootprint(parent::get($key));
}
/**
* Returns storage used for cache.
*
* @return StorageInterface
*/
public function getStorage(): StorageInterface
{
return $this->storage;
}
/**
* Returns the computed footprint of a value.
*
* @param mixed $value
*
* @return string
*/
private function computeFootprint($value)
{
return sha1(serialize($value));
}
/**
* Returns known footprint for a cached item.
*
* @param string $key
*
* @return string|null
*/
private function getCachedFootprint($key)
{
$footprints = $this->getAllCachedFootprints();
return array_key_exists($key, $footprints) ? $footprints[$key] : null;
}
/**
* Defines footprint for cache item.
*
* @param string $key Key of the cached item.
* @param mixed $values Value of the cached item.
*
* @return void
*/
private function setFootprint($key, $value)
{
$this->setMultipleFootprints([$key => $value]);
}
/**
* Defines footprint for multiple cache items.
*
* @param array $values Associative array of cached items, where keys corresponds to the
* cache key of the item and value is its cached value.
*
* @return void
*/
private function setMultipleFootprints(array $values)
{
$footprints = $this->getAllCachedFootprints();
foreach ($values as $key => $value) {
$footprints[$key] = $this->computeFootprint($value);
}
$this->setAllCachedFootprints($footprints);
}
/**
* Check footprint file integrity, to ensure that it can be used securely.
*
* @return void
*/
private function checkFootprintFileIntegrity()
{
if ((file_exists($this->footprint_file) && ! is_writable($this->footprint_file))
|| (! file_exists($this->footprint_file) && ! is_writable(dirname($this->footprint_file)))) {
trigger_error(
sprintf('Cannot write "%s" cache footprint file. Cache performance can be lowered.', $this->footprint_file),
E_USER_WARNING
);
$this->footprint_file = null;
return;
}
if (! file_exists($this->footprint_file)) {
// Create empty array in file if not exists.
$this->setAllCachedFootprints([]);
return;
}
$file_contents = file_get_contents($this->footprint_file);
if (empty($file_contents)) {
// Create empty array in file if empty.
$this->setAllCachedFootprints([]);
return;
}
$footprints = json_decode($file_contents, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($footprints)) {
// Clear footprint file if not a valid JSON.
trigger_error(
sprintf('Cache footprint file "%s" contents was invalid, it has been cleaned.', $this->footprint_file),
E_USER_WARNING
);
$this->setAllCachedFootprints([]);
}
}
/**
* Returns all cache footprints.
*
* @return array Associative array of cached items footprints, where keys corresponds to the
* cache key of the item and value is its footprint.
*/
private function getAllCachedFootprints()
{
if (null !== $this->footprint_file) {
$file_contents = file_get_contents($this->footprint_file);
$footprints = json_decode($file_contents, true);
if (json_last_error() !== JSON_ERROR_NONE || ! is_array($footprints)) {
// Should happen only if file has been corrupted after cache instanciation,
// launch integrity tests again to trigger warnings and fix file contents.
$this->checkFootprintFileIntegrity();
return [];
}
return $footprints;
}
return $this->footprint_fallback_storage;
}
/**
* Save all cache footprints.
*
* @param array $footprints
* Associative array of cached items footprints, where keys corresponds to the
* cache key of the item and value is its footprint.
*
* @return void
*/
private function setAllCachedFootprints($footprints)
{
if (null !== $this->footprint_file) {
// Remove null values to prevent storage of deleted footprints
array_filter(
$footprints,
function ($val) {
return null !== $val;
}
);
$json = json_encode($footprints, JSON_PRETTY_PRINT);
$handle = fopen($this->footprint_file, 'c');
$is_locked = flock($handle, LOCK_EX); // Lock the file, if possible (depends on used FS)
$result = ftruncate($handle, 0)
&& fwrite($handle, $json)
&& fflush($handle);
if ($is_locked) {
// Unlock the file if it has been locked
flock($handle, LOCK_UN);
}
fclose($handle);
if ($result !== false) {
return;
} else {
// Should happen only if file is not writable anymore (rights problems or no more disk space),
// fallback to singleton storage.
$this->footprint_file = null;
}
}
$this->footprint_fallback_storage = $footprints;
}
}
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2018 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Cache;
use Zend\Cache\Exception\ExceptionInterface;
/**
* Glpi simple cache factory.
*
* @since 10.0.0
*/
class SimpleCacheFactory
{
/**
* Cache directory.
*
* @var string
*/
private $cacheDir;
/**
* Force skipping of integrity checks.
*
* @var bool
*/
private $disableIntegrityChecks;
/**
* Cache storage factory class.
*
* @var CacheStorageFactory
*/
private $storageFactory;
/**
* @param string $cacheDir Cache directory.
* @param bool $disableIntegrityChecks Force skipping of integrity checks.
* @param CacheStorageFactory $storageFactory Cache storage factory class.
*/
public function __construct(string $cacheDir, bool $disableIntegrityChecks, CacheStorageFactory $storageFactory)
{
$this->cacheDir = $cacheDir;
$this->disableIntegrityChecks = $disableIntegrityChecks;
$this->storageFactory = $storageFactory;
}
/**
* Create a simple cache instance.
*
* @param array $cfg Cache storage configuration, see Zend\Cache\StorageFactory::factory()
*
* @return SimpleCache
*/
public function factory($cfg): SimpleCache
{
$isAdapterComputed = !isset($cfg['adapter']) || 'auto' === $cfg['adapter'];
$skipIntegrityChecks = $this->disableIntegrityChecks
|| (!$isAdapterComputed && $this->canSkipIntegrityChecks($cfg['adapter']));
// Compute prefered adapter if 'auto' value or no value is used
if ($isAdapterComputed) {
if (function_exists('wincache_ucache_add')) {
$cfg['adapter'] = 'wincache';
} elseif (function_exists('apcu_fetch')) {
$cfg['adapter'] = 'apcu';
} else {
$cfg['adapter'] = 'filesystem';
}
}
$namespace = isset($cfg['options']) && isset($cfg['options']['namespace'])
? $cfg['options']['namespace']
: '_default';
try {
$storage = $this->storageFactory->factory($cfg);
} catch (ExceptionInterface $e) {
if ($isAdapterComputed && 'filesystem' !== $cfg['adapter']) {
// Fallback to 'filesystem' adapter if adapter was not explicitely defined in config
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "filesystem" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
$storage = $this->storageFactory->factory(
[
'adapter' => 'filesystem',
'options' => [
'namespace' => $namespace . '_fallback'
],
]
);
} else {
// Fallback to 'memory' adapter
trigger_error(
sprintf(
'Cache adapter instantiation failed, fallback to "memory" adapter. Error was "%s".',
$e->getMessage()
),
E_USER_WARNING
);
$storage = $this->storageFactory->factory(
[
'adapter' => 'memory',
'options' => [
'namespace' => $namespace . '_fallback'
],
]