diff options
-rw-r--r-- | Auth/OpenID/Interface.php | 8 | ||||
-rw-r--r-- | Auth/OpenID/MemcachedStore.php | 207 | ||||
-rw-r--r-- | Tests/Auth/OpenID/MemStore.php | 2 | ||||
-rw-r--r-- | Tests/Auth/OpenID/StoreTest.php | 47 |
4 files changed, 261 insertions, 3 deletions
diff --git a/Auth/OpenID/Interface.php b/Auth/OpenID/Interface.php index eeb130a..38fe369 100644 --- a/Auth/OpenID/Interface.php +++ b/Auth/OpenID/Interface.php @@ -94,6 +94,14 @@ class Auth_OpenID_OpenIDStore { } /** + * Report whether this storage supports cleanup + */ + function supportsCleanup() + { + return true; + } + + /** * This method returns an Association object from storage that * matches the server URL and, if specified, handle. It returns * null if no such association is found or if the matching diff --git a/Auth/OpenID/MemcachedStore.php b/Auth/OpenID/MemcachedStore.php new file mode 100644 index 0000000..7f9b180 --- /dev/null +++ b/Auth/OpenID/MemcachedStore.php @@ -0,0 +1,207 @@ +<?php + +/** + * This file supplies a memcached store backend for OpenID servers and + * consumers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author Artemy Tregubenko <me@arty.name> + * @copyright Open Web Technologies <http://openwebtech.ru/> + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Import the interface for creating a new store class. + */ +require_once 'Auth/OpenID/Interface.php'; + +/** + * This is a memcached-based store for OpenID associations and + * nonces. + * + * As memcache has limit of 250 chars for key length, + * server_url, handle and salt are hashed with sha1(). + * + * Most of the methods of this class are implementation details. + * People wishing to just use this store need only pay attention to + * the constructor. + * + * @package OpenID + */ +class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore { + + /** + * Initializes a new {@link Auth_OpenID_MemcachedStore} instance. + * Just saves memcached object as property. + * + * @param resource connection Memcache connection resourse + */ + function Auth_OpenID_MemcachedStore($connection, $compress = false) + { + $this->connection = $connection; + $this->compress = $compress ? MEMCACHE_COMPRESSED : 0; + } + + /** + * Store association until its expiration time in memcached. + * Overwrites any existing association with same server_url and + * handle. Handles list of associations for every server. + */ + function storeAssociation($server_url, $association) + { + // create memcached keys for association itself + // and list of associations for this server + $associationKey = $this->associationKey($server_url, + $association->handle); + $serverKey = $this->associationServerKey($server_url); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + + // if no such list, initialize it with empty array + if (!$serverAssociations) { + $serverAssociations = array(); + } + // and store given association key in it + $serverAssociations[$association->issued] = $associationKey; + + // save associations' keys list + $this->connection->set( + $serverKey, + $serverAssociations, + $this->compress + ); + // save association itself + $this->connection->set( + $associationKey, + $association, + $this->compress, + $association->issued + $association->lifetime); + } + + /** + * Read association from memcached. If no handle given + * and multiple associations found, returns latest issued + */ + function getAssociation($server_url, $handle = null) + { + // simple case: handle given + if ($handle !== null) { + // get association, return null if failed + $association = $this->connection->get( + $this->associationKey($server_url, $handle)); + return $association ? $association : null; + } + + // no handle given, working with list + // create key for list of associations + $serverKey = $this->associationServerKey($server_url); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + // return null if failed or got empty list + if (!$serverAssociations) { + return null; + } + + // get key of most recently issued association + $keys = array_keys($serverAssociations); + sort($keys); + $lastKey = $serverAssociations[array_pop($keys)]; + + // get association, return null if failed + $association = $this->connection->get($lastKey); + return $association ? $association : null; + } + + /** + * Immediately delete association from memcache. + */ + function removeAssociation($server_url, $handle) + { + // create memcached keys for association itself + // and list of associations for this server + $serverKey = $this->associationServerKey($server_url); + $associationKey = $this->associationKey($server_url, + $handle); + + // get list of associations + $serverAssociations = $this->connection->get($serverKey); + // return null if failed or got empty list + if (!$serverAssociations) { + return false; + } + + // ensure that given association key exists in list + $serverAssociations = array_flip($serverAssociations); + if (!array_key_exists($associationKey, $serverAssociations)) { + return false; + } + + // remove given association key from list + unset($serverAssociations[$associationKey]); + $serverAssociations = array_flip($serverAssociations); + + // save updated list + $this->connection->set( + $serverKey, + $serverAssociations, + $this->compress + ); + + // delete association + return $this->connection->delete($associationKey); + } + + /** + * Create nonce for server and salt, expiring after + * $Auth_OpenID_SKEW seconds. + */ + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + // save one request to memcache when nonce obviously expired + if (abs($timestamp - time()) > $Auth_OpenID_SKEW) { + return false; + } + + // returns false when nonce already exists + // otherwise adds nonce + return $this->connection->add( + 'openid_nonce_' . sha1($server_url) . '_' . sha1($salt), + 1, // any value here + $this->compress, + $Auth_OpenID_SKEW); + } + + /** + * Memcache key is prefixed with 'openid_association_' string. + */ + function associationKey($server_url, $handle = null) + { + return 'openid_association_' . sha1($server_url) . '_' . sha1($handle); + } + + /** + * Memcache key is prefixed with 'openid_association_' string. + */ + function associationServerKey($server_url) + { + return 'openid_association_server_' . sha1($server_url); + } + + /** + * Report that this storage doesn't support cleanup + */ + function supportsCleanup() + { + return false; + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/MemStore.php b/Tests/Auth/OpenID/MemStore.php index b70a6dd..83fdb1b 100644 --- a/Tests/Auth/OpenID/MemStore.php +++ b/Tests/Auth/OpenID/MemStore.php @@ -75,7 +75,7 @@ class ServerAssocs { * * Use for single long-running processes. No persistence supplied. */ -class Tests_Auth_OpenID_MemStore { +class Tests_Auth_OpenID_MemStore extends Auth_OpenID_OpenIDStore { function Tests_Auth_OpenID_MemStore() { $this->server_assocs = array(); diff --git a/Tests/Auth/OpenID/StoreTest.php b/Tests/Auth/OpenID/StoreTest.php index aabb3a0..0848c0b 100644 --- a/Tests/Auth/OpenID/StoreTest.php +++ b/Tests/Auth/OpenID/StoreTest.php @@ -47,6 +47,12 @@ global $_Auth_OpenID_db_test_host; $_Auth_OpenID_db_test_host = 'dbtest'; /** + * This is the host where the Memcache stores should save data + */ +global $_Auth_OpenID_memcache_test_host; +$_Auth_OpenID_memcache_test_host = 'localhost'; + +/** * Generate a sufficently unique database name so many hosts can run * SQL store tests on the server at the same time and not step on each * other. @@ -291,6 +297,10 @@ explicitly'); $assocExpired1 = $this->genAssoc($now, -7200, 3600); $assocExpired2 = $this->genAssoc($now, -7200, 3600); + if (!$store->supportsCleanup()) { + return; + } + $store->cleanupAssociations(); $store->storeAssociation($server_url . '1', $assocValid1); $store->storeAssociation($server_url . '1', $assocExpired1); @@ -337,8 +347,10 @@ explicitly'); } function _testNonceCleanup(&$store) { - global $Auth_OpenID_SKEW; - + if (!$store->supportsCleanup()) { + return; + } + $server_url = 'http://www.myopenid.com/openid'; $now = time(); @@ -419,6 +431,37 @@ explicitly'); $store->destroy(); } + function test_memcachedstore() + { + // If the memcache extension isn't loaded or loadable, succeed + // because we can't run the test. + if (!(extension_loaded('memcache') || + @dl('memcache.so') || + @dl('php_memcache.dll'))) { + print "Warning: not testing Memcache store"; + $this->pass(); + return; + } + + global $_Auth_OpenID_memcache_test_host; + + $memcached = new Memcache(); + if (!$memcached->connect($_Auth_OpenID_memcache_test_host)) { + $this->fail("Couldn't connect to Memcache server at '". + $_Auth_OpenID_memcache_test_host); + } + + require_once 'Auth/OpenID/MemcachedStore.php'; + + $store = new Auth_OpenID_MemcachedStore($memcached); + + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + + $memcached->close(); + } + function test_postgresqlstore() { // If the postgres extension isn't loaded or loadable, succeed |