diff --git a/lib/SimpleSAML/MemcacheStore.php b/lib/SimpleSAML/MemcacheStore.php new file mode 100644 index 0000000000000000000000000000000000000000..7ae8e9299e6d9aa74b2b7d7fe0eccf965176b4ac --- /dev/null +++ b/lib/SimpleSAML/MemcacheStore.php @@ -0,0 +1,563 @@ +<? + +/* + * This file implements a storage class which stores data to one or more + * groups memcache servers. + * + * The goals of this storage class is to provide failover, redudancy and load + * balancing. This is accomplished by storing the data object to several + * groups of memcache servers. Each data object is replicated to every group + * of memcache servers, but it is only stored to one server in each group. + * + * For this code to work correctly, all web servers accessing the data must + * have the same clock (as measured by the time()-function). Different clock + * values will lead to incorrect behaviour. + */ + +/* We need access to the configuration from config/config.php. */ +require_once('SimpleSAML/Configuration.php'); + + +class SimpleSAML_MemcacheStore { + + + /* This variable contains the last commit time of this object. + * This is used to determine which of the memcache servers contain + * the newest version. + * + * This variable is serialized. + */ + private $lastCommitTime = NULL; + + + /* This variable contains the id for this data. + * + * This variable is serialized. + */ + private $id = NULL; + + + /* This variable contains an array with all key-value pairs stored + * in this object. + * + * This variable is serialized. + */ + private $data = NULL; + + + /* This variable contains the serialized data which is currently + * stored on the memcache servers. By comparing the data which is + * stored against the current data, we can determine whether we + * should update the data. + * + * If this variable is NULL, then we need to store data to the + * memcache servers. + * + * This variable isn't serialized. + */ + private $savedData = NULL; + + + /* Private cache of the memcache servers we are using. */ + private static $serverGroups = NULL; + + + + /* This function is used to find an existing storage object. It will + * return NULL if no storage object with the given id is found. + * + * Parameters: + * $id The id of the storage object we are looking for. A id + * consists of lowercase alphanumeric characters. + * + * Returns: + * The corresponding MemcacheStorage object if the data is found + * or NULL if it isn't found. + */ + public static function find($id) { + assert(self::isValidID($id)); + + $mustUpdate = FALSE; + $latest = NULL; + $latestSerializedValue = NULL; + + /* Search all the servers for the given id. */ + foreach(self::getMemcacheServers() as $server) { + $serializedValue = $server->get($id); + if($serializedValue === FALSE) { + /* Either the server is down, or we don't have + * the value stored on that server. + */ + $mustUpdate = TRUE; + continue; + } + + /* Deserialize the object. */ + $v = unserialize($serializedValue); + + /* Make sure that the deserialized object is of the + * correct type. + */ + if(!($v instanceof self)) { + $e = 'We retrieved an object from the' . + ' memcache server which wasn\'t an' . + ' instance of a MemcacheStore object.' . + ' This should never happen, and is a ' . + ' sign that the MemcacheStore may be ' . + ' unreliable.'; + error_log($e); + die($e); + } + + if($latest === NULL) { + $latest = $v; + $latestSerializedValue = $serializedValue; + continue; + } + + if($latest->lastCommitTime == $v->lastCommitTime) { + /* They were committed at the same time. Assume + * that they are equal. + */ + continue; + } + + /* They are different. We need to update at least one + * of them to maintain synchronization. + */ + + $mustUpdate = TRUE; + + /* Update $latest if $v is newer than $latest. */ + if($latest->lastCommitTime < $v->lastCommitTime) { + $latest = $v; + $latestSerializedValue = $serializedValue; + } + } + + /* Check if we found any stored object matching the id. */ + if($latest === NULL) { + return NULL; + } + + /* If we don't need to update this data object, then we can + * store the serialized value in the MemcacheStore object. If + * we need to update this object, then we don't store the + * serialized value. + */ + if($mustUpdate === TRUE) { + $latest->savedData = $latestSerializedValue; + } + + return $latest; + } + + + /* This constructor is used to create a new storage object. The storage + * object will be created with the specified id and the initial + * content passed in the data argument. + * + * If there exists a storage object with the specified id, then it will + * be overwritten. + * + * Parameters: + * $id The id of the storage object. + * $data An array containing the initial data of the storage + * object. + */ + public function __construct($id, $data = array()) { + /* Validate arguments. */ + assert(self::isValidID($id)); + assert(is_array($data)); + + $this->id = $id; + $this->data = $data; + + /* Add a call to save the data when we exit. */ + register_shutdown_function(array($this, 'save')); + } + + + /* This magic function is called on serialization of this class. + * It returns a list of the names of the variables which should be + * serialized. + */ + private function __sleep() { + return array('lastCommitTime', 'id', 'data'); + } + + + /* This function retrieves the specified key from this storage object. + * + * Parameters: + * $key The key we should retrieve the value of. + * + * Returns: + * The value of the specified key, or NULL of the key wasn't found. + */ + public function get($key) { + if(!array_key_exists($key, $this->data)) { + return NULL; + } + + return $this->data[$key]; + } + + + /* This function sets the specified key to the specified value in this + * storage object. + * + * Parameters: + * $key The key we should set. + * $value The value we should set the key to. + */ + public function set($key, $value) { + $this->data[$key] = $value; + + /* Set savedData to NULL. This will save time when + * we are going to decide whether we need to update this + * object on the memcache servers. + */ + $this->savedData = NULL; + } + + + /* This function determines whether we need to update the data which + * is stored on the memcache servers. + * + * If we are unable to detect a change, then we will serialize the + * class and compare this to the data we have cached. We do this to + * determine if any of the references have changed. + * + * Returns: + * TRUE if this object needs an update, FALSE if not. + */ + private function needUpdate() { + /* If $savedData is NULL, then we don't have any data stored + * on any servers. Therefore, we need to update the data. + */ + if($this->savedData === NULL) { + return TRUE; + } + + /* Calculate the serialized value of this object. */ + $serialized = serialize($this); + + /* If the serialized value of this object matches the previous + * serialized value, then we don't need to update the data on + * the servers. + */ + if($serialized === $this->savedData) { + return FALSE; + } + + /* We need to store the updated value to the servers. */ + return TRUE; + } + + + /* This function stores this storage object to the memcache servers. + */ + public function save() { + /* First, chech whether we need to store new data. */ + if(!$this->needUpdate()) { + /* This object is unchanged - we don't need to + * commit. + */ + return; + } + + /* Update the last-commit timestamp. */ + $this->lastCommitTime = time(); + + /* Calculate the value we should store on the servers. */ + $this->savedData = serialize($this); + + /* Store this object to all groups of memcache servers. */ + foreach(self::getMemcacheServers() as $server) { + $server->set($this->id, $this->savedData); + } + } + + + /* This function adds a server from the 'memcache_store.servers' + * configuration option to a Memcache object. + * + * Parameters: + * $memcache The Memcache object we should add this server to. + * $server The server we should parse. This is an array with + * the following keys: + * - hostname + * Hostname or ip address to the memcache server. + * - port (optional) + * port number the memcache server is running on. This + * defaults to memcache.default_port if no value is given. + * The default value of memcache.default_port is 11211. + * - weight (optional) + * The weight of this server in the load balancing + * cluster. + * - timeout (optional) + * The timeout for contacting this server, in seconds. + * The default value is 3 seconds. + */ + private static function addMemcacheServer($memcache, $server) { + + /* The hostname option is required. */ + if(!array_key_exists('hostname', $server)) { + $e = 'hostname setting missing from server in the' . + ' \'memcache_store.servers\' configuration' . + ' option.'; + error_log($e); + die($e); + } + + $hostname = $server['hostname']; + + /* The hostname must be a valid string. */ + if(!is_string($hostname)) { + $e = 'Invalid hostname for server in the' . + ' \'memcache_store.servers\' configuration' . + ' option. The hostname is supposed to be a' . + ' string.'; + error_log($e); + die($e); + } + + /* Check if the user has specified a port number. */ + if(array_key_exists('port', $server)) { + /* Get the port number from the array, and validate + * it. + */ + $port = (int)$server['port']; + if(($port < 0) || ($port > 65535)) { + $e = 'Invalid port for server in the' . + ' \'memcache_store.servers\'' . + ' configuration option. The port number' . + ' is supposed to be an integer between' . + ' 0 and 65535.'; + error_log($e); + die($e); + } + } else { + /* Use the default port number from the ini-file. */ + $port = (int)ini_get('memcache.default_port'); + if($port <= 0 || $port > 65535) { + /* Invalid port number from the ini-file. + * fall back to the default. + */ + $port = 11211; + } + } + + /* Check if the user has specified a weight for this server. */ + if(array_key_exists('weight', $server)) { + /* Get the weight and validate it. */ + $weight = (int)$server['weight']; + if($weight <= 0) { + $e = 'Invalid weight for server in the' . + ' \'memcache_store.servers\'' . + ' configuration option. The weight is' . + ' supposed to be a positive integer.'; + error_log($e); + die($e); + } + } else { + /* Use a default weight of 1. */ + $weight = 1; + } + + /* Check if the user has specified a timeout for this + * server. + */ + if(array_key_exists('timeout', $server)) { + /* Get the timeout and validate it. */ + $timeout = (int)$server['timeout']; + if($timeout <= 0) { + $e = 'Invalid timeout for server in the' . + ' \'memcache_store.servers\'' . + ' configuration option. The timeout is' . + ' supposed to be a positive integer.'; + error_log($e); + die($e); + } + } else { + /* Use a default timeout of 3 seconds. */ + $timeout = 3; + } + + /* Add this server to the Memcache object. */ + $memcache->addServer($hostname, $port, TRUE, $weight, $timeout); + } + + + /* This function takes in a list of servers belonging to a group and + * creates a Memcache object from the servers in the group. + * + * Parameters: + * $group Array of servers. Each server is represented by one array + * with the hostname (and optionally port number, timeout, + * ...). See the addMemcacheServer function for more + * information. + * + * Returns: + * A Memcache object of the servers in the group. + */ + private static function loadMemcacheServerGroup($group) { + /* Create the Memcache object. */ + $memcache = new Memcache(); + if($memcache == NULL) { + $e = 'Unable to create an instance of a Memcache' . + ' object. Is the memcache extension' . + ' installed?'; + error_log($e); + die($e); + } + + /* Iterate over all the servers in the group and add them to + * the Memcache object. + */ + foreach($group as $index => $server) { + /* Make sure that we don't have an index. An index + * would be a sign of invalid configuration. + */ + if(!is_int($index)) { + $e = 'Invalid index on element in the' . + ' \'memcache_store.servers\'' . + ' configuration option. Perhaps you' . + ' have forgotten to add an array(...)' . + ' around one of the server groups? The' . + ' invalid index was: ' . $index; + error_log($e); + die($e); + } + + /* Make sure that the server object is an array. Each + * server is an array with name-value pairs. + */ + if(!is_array($server)) { + $e = 'Invalid value for the server with' . + ' index ' . $index . '. Remeber that' . + ' the \'memcache_store.servers\'' . + ' configuration option contains an' . + ' array of arrays of arrays.'; + error_log($e); + die($e); + } + + self::addMemcacheServer($memcache, $server); + } + + return $memcache; + } + + + /* This function gets a list of all configured memcache servers. This + * list is initialized based on the content of + * 'memcache_store.servers' in the configuration. + * + * Returns: + * Array with Memcache objects. + */ + private static function getMemcacheServers() { + + /* Check if we have loaded the servers already. */ + if(self::$serverGroups != NULL) { + return self::$serverGroups; + } + + /* Initialize the servers-array. */ + self::$serverGroups = array(); + + /* Load the configuration. */ + $config = SimpleSAML_Configuration::getInstance(); + assert($config instanceof SimpleSAML_Configuration); + + + $groups = $config->getValue('memcache_store.servers'); + + /* Validate the 'memcache_store.servers' configuration + * option. + */ + if(is_null($groups)) { + $e = 'Unable to get value of the' . + ' \'memcache_store.servers\' configuration' . + ' option.'; + error_log($e); + die($e); + } + if(!is_array($groups)) { + $e = 'The value of the \'memcache_store.servers\'' . + ' configuration option isn\'t an array.'; + error_log($e); + die($e); + } + + /* Iterate over all the groups in the + * 'memcache_store.servers' configuration option. + */ + foreach($groups as $index => $group) { + /* Make sure that the group doesn't have an index. + * An index would be a sign of invalid configuration. + */ + if(!is_int($index)) { + $e = 'Invalid index on element in the' . + ' \'memcache_store.servers\'' . + ' configuration option. Perhaps you' . + ' have forgotten to add an array(...)' . + ' around one of the server groups? The' . + ' invalid index was: ' . $index; + error_log($e); + die($e); + } + + /* Make sure that the group is an array. Each group + * is an array of servers. Each server is an array of + * name => value pairs for that server. + */ + if(!is_array($group)) { + $e = 'Invalid value for the server with' . + ' index ' . $index . '. Remeber that' . + ' the \'memcache_store.servers\'' . + ' configuration option contains an' . + ' array of arrays of arrays.'; + error_log($e); + die($e); + } + + /* Parse and add this group to the server group list. + */ + self::$serverGroups[] = + self::loadMemcacheServerGroup($group); + } + + return self::$serverGroups; + } + + + /* This function determines whether the argument is a valid id. + * A valid id is a string containing lowercase alphanumeric + * characters. + * + * Parameters: + * $id The id we should validate. + * + * Returns: + * TRUE if the id is valid, FALSE otherwise. + */ + private static function isValidID($id) { + if(!is_string($id)) { + return FALSE; + } + + if(strlen($id) < 1) { + return FALSE; + } + + if(preg_match('/[^0-9a-z]/', $id)) { + return FALSE; + } + + return TRUE; + } +} +?>