diff options
author | Pavlo Yatsukhnenko <yatsukhnenko@gmail.com> | 2018-03-24 18:45:51 +0300 |
---|---|---|
committer | Pavlo Yatsukhnenko <yatsukhnenko@gmail.com> | 2018-03-24 18:45:51 +0300 |
commit | 300c72510c48e210338826b713f260a4eda8abc7 (patch) | |
tree | 2addd5f65234763f6ea0ee72c48b97000b3894e8 | |
parent | fd93e26fc42ae0be6c5d5510516933e7d9350722 (diff) | |
parent | 29edc7db158412239aa5f36a63f342fdcac6c13c (diff) |
Merge branch 'session-locking' into develop
Conflicts:
redis_session.c
tests/RedisTest.php
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | php_redis.h | 5 | ||||
-rw-r--r-- | redis.c | 10 | ||||
-rw-r--r-- | redis_session.c | 349 | ||||
-rw-r--r-- | redis_session.h | 2 | ||||
-rw-r--r-- | tests/RedisClusterTest.php | 20 | ||||
-rw-r--r-- | tests/RedisTest.php | 376 | ||||
-rw-r--r-- | tests/getSessionData.php | 19 | ||||
-rw-r--r-- | tests/regenerateSessionId.php | 83 | ||||
-rw-r--r-- | tests/startSession.php | 38 |
10 files changed, 875 insertions, 28 deletions
@@ -14,3 +14,4 @@ missing autom4te.cache mkinstalldirs run-tests.php +idea/*
\ No newline at end of file diff --git a/php_redis.h b/php_redis.h index ea60c3a2..b076e51b 100644 --- a/php_redis.h +++ b/php_redis.h @@ -262,11 +262,6 @@ PHP_REDIS_API int redis_sock_read_multibulk_multi_reply_loop( INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, int numElems); -#ifndef _MSC_VER -ZEND_BEGIN_MODULE_GLOBALS(redis) -ZEND_END_MODULE_GLOBALS(redis) -#endif - extern zend_module_entry redis_module_entry; #define redis_module_ptr &redis_module_entry @@ -73,6 +73,12 @@ PHP_INI_BEGIN() PHP_INI_ENTRY("redis.clusters.read_timeout", "", PHP_INI_ALL, NULL) PHP_INI_ENTRY("redis.clusters.seeds", "", PHP_INI_ALL, NULL) PHP_INI_ENTRY("redis.clusters.timeout", "", PHP_INI_ALL, NULL) + + /* redis session */ + PHP_INI_ENTRY("redis.session.locking_enabled", "", PHP_INI_ALL, NULL) + PHP_INI_ENTRY("redis.session.lock_expire", "", PHP_INI_ALL, NULL) + PHP_INI_ENTRY("redis.session.lock_retries", "", PHP_INI_ALL, NULL) + PHP_INI_ENTRY("redis.session.lock_wait_time", "", PHP_INI_ALL, NULL) PHP_INI_END() /** {{{ Argument info for commands in redis 1.0 */ @@ -226,10 +232,6 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_kscan, 0, 0, 2) ZEND_ARG_INFO(0, i_count) ZEND_END_ARG_INFO() -#ifdef ZTS -ZEND_DECLARE_MODULE_GLOBALS(redis) -#endif - static zend_function_entry redis_functions[] = { PHP_ME(Redis, __construct, arginfo_void, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC) PHP_ME(Redis, __destruct, arginfo_void, ZEND_ACC_DTOR | ZEND_ACC_PUBLIC) diff --git a/redis_session.c b/redis_session.c index 6a1dce78..6a03f853 100644 --- a/redis_session.c +++ b/redis_session.c @@ -41,13 +41,40 @@ #include "SAPI.h" #include "ext/standard/url.h" +/* HOST_NAME_MAX doesn't exist everywhere */ +#ifndef HOST_NAME_MAX + #if defined(_POSIX_HOST_NAME_MAX) + #define HOST_NAME_MAX _POSIX_HOST_NAME_MAX + #elif defined(MAXHOSTNAMELEN) + #define HOST_NAME_MAX MAXHOSTNAMELEN + #else + #define HOST_NAME_MAX 255 + #endif +#endif + +/* Session lock LUA as well as its SHA1 hash */ +#define LOCK_RELEASE_LUA_STR "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end" +#define LOCK_RELEASE_LUA_LEN (sizeof(LOCK_RELEASE_LUA_STR) - 1) +#define LOCK_RELEASE_SHA_STR "b70c2384248f88e6b75b9f89241a180f856ad852" +#define LOCK_RELEASE_SHA_LEN (sizeof(LOCK_RELEASE_SHA_STR) - 1) + +/* Check if a response is the Redis +OK status response */ +#define IS_REDIS_OK(r, len) (r != NULL && len == 3 && !memcmp(r, "+OK", 3)) + ps_module ps_mod_redis = { - PS_MOD(redis) + PS_MOD_SID(redis) }; ps_module ps_mod_redis_cluster = { PS_MOD(rediscluster) }; +typedef struct { + zend_bool is_locked; + char *session_key; + char *lock_key; + char *lock_secret; +} redis_session_lock_status; + typedef struct redis_pool_member_ { RedisSock *redis_sock; @@ -65,6 +92,7 @@ typedef struct { int count; redis_pool_member *head; + redis_session_lock_status lock_status; } redis_pool; @@ -78,7 +106,6 @@ redis_pool_add(redis_pool *pool, RedisSock *redis_sock, int weight, rpm->database = database; rpm->prefix = prefix; - rpm->auth = auth; rpm->next = pool->head; @@ -101,9 +128,35 @@ redis_pool_free(redis_pool *pool TSRMLS_DC) { efree(rpm); rpm = next; } + + /* Cleanup after our lock */ + if (pool->lock_status.session_key) + efree(pool->lock_status.session_key); + if (pool->lock_status.lock_secret) + efree(pool->lock_status.lock_secret); + if (pool->lock_status.lock_key) + efree(pool->lock_status.lock_key); + + /* Cleanup pool itself */ efree(pool); } +/* Send a command to Redis. Returns reply on success and NULL on failure */ +static char *redis_simple_cmd(RedisSock *redis_sock, char *cmd, int cmdlen, + int *replylen TSRMLS_DC) +{ + char *reply; + + if (redis_sock_write(redis_sock, cmd, cmdlen TSRMLS_CC) >= 0) { + if ((reply = redis_sock_read(redis_sock, replylen TSRMLS_CC)) != NULL) { + return reply; + } + } + + /* Failed to send or receive command */ + return NULL; +} + static void redis_pool_member_auth(redis_pool_member *rpm TSRMLS_DC) { RedisSock *redis_sock = rpm->redis_sock; @@ -171,6 +224,182 @@ redis_pool_get_sock(redis_pool *pool, const char *key TSRMLS_DC) { return NULL; } +/* Helper to set our session lock key */ +static int set_session_lock_key(RedisSock *redis_sock, char *cmd, int cmd_len + TSRMLS_DC) +{ + char *reply; + int reply_len; + + reply = redis_simple_cmd(redis_sock, cmd, cmd_len, &reply_len TSRMLS_CC); + if (reply) { + if (IS_REDIS_OK(reply, reply_len)) { + efree(reply); + return SUCCESS; + } + + efree(reply); + } + + return FAILURE; +} + +static int lock_acquire(RedisSock *redis_sock, redis_session_lock_status *lock_status + TSRMLS_DC) +{ + char *cmd, hostname[HOST_NAME_MAX] = {0}; + int cmd_len, lock_wait_time, retries, i, expiry; + + /* Short circuit if we are already locked or not using session locks */ + if (lock_status->is_locked || !INI_INT("redis.session.locking_enabled")) + return SUCCESS; + + /* How long to wait between attempts to acquire lock */ + lock_wait_time = INI_INT("redis.session.lock_wait_time"); + if (lock_wait_time == 0) { + lock_wait_time = 2000; + } + + /* Maximum number of times to retry (-1 means infinite) */ + retries = INI_INT("redis.session.lock_retries"); + if (retries == 0) { + retries = 10; + } + + /* How long should the lock live (in seconds) */ + expiry = INI_INT("redis.session.lock_expire"); + if (expiry == 0) { + expiry = INI_INT("max_execution_time"); + } + + /* Generate our qualified lock key */ + spprintf(&lock_status->lock_key, 0, "%s%s", lock_status->session_key, "_LOCK"); + + /* Calculate lock secret */ + gethostname(hostname, HOST_NAME_MAX); + spprintf(&lock_status->lock_secret, 0, "%s|%ld", hostname, (long)getpid()); + + if (expiry > 0) { + cmd_len = REDIS_SPPRINTF(&cmd, "SET", "ssssd", lock_status->lock_key, + strlen(lock_status->lock_key), lock_status->lock_secret, + strlen(lock_status->lock_secret), "NX", 2, + "PX", 2, expiry * 1000); + } else { + cmd_len = REDIS_SPPRINTF(&cmd, "SET", "sss", lock_status->lock_key, + strlen(lock_status->lock_key), lock_status->lock_secret, + strlen(lock_status->lock_secret), "NX", 2); + } + + /* Attempt to get our lock */ + for (i = 0; retries == -1 || i <= retries; i++) { + if (set_session_lock_key(redis_sock, cmd, cmd_len TSRMLS_CC) == SUCCESS) { + lock_status->is_locked = 1; + break; + } + + /* Sleep unless we're done making attempts */ + if (retries == -1 || i < retries) { + usleep(lock_wait_time); + } + } + + /* Cleanup SET command */ + efree(cmd); + + /* Success if we're locked */ + return lock_status->is_locked ? SUCCESS : FAILURE; +} + +#define IS_LOCK_SECRET(reply, len, secret) (len == strlen(secret) && !strncmp(reply, secret, len)) +static void refresh_lock_status(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC) +{ + char *cmd, *reply = NULL; + int replylen, cmdlen; + + /* Return early if we're not locked */ + if (!lock_status->is_locked) + return; + + /* If redis.session.lock_expire is not set => TTL=max_execution_time + Therefore it is guaranteed that the current process is still holding + the lock */ + if (lock_status->is_locked && INI_INT("redis.session.lock_expire") == 0) + return; + + /* Command to get our lock key value and compare secrets */ + cmdlen = REDIS_SPPRINTF(&cmd, "GET", "s", lock_status->lock_key, + strlen(lock_status->lock_key)); + + /* Attempt to refresh the lock */ + reply = redis_simple_cmd(redis_sock, cmd, cmdlen, &replylen TSRMLS_CC); + if (reply != NULL) { + lock_status->is_locked = IS_LOCK_SECRET(reply, replylen, lock_status->lock_secret); + efree(reply); + } else { + lock_status->is_locked = 0; + } + + /* Issue a warning if we're not locked. We don't attempt to refresh the lock + * if we aren't flagged as locked, so if we're not flagged here something + * failed */ + if (!lock_status->is_locked) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to refresh session lock"); + } + + /* Cleanup */ + efree(cmd); +} + +static int write_allowed(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC) +{ + if (!INI_INT("redis.session.locking_enabled")) + return 1; + + refresh_lock_status(redis_sock, lock_status TSRMLS_CC); + + return lock_status->is_locked; +} + +/* Release any session lock we hold and cleanup allocated lock data. This function + * first attempts to use EVALSHA and then falls back to EVAL if EVALSHA fails. This + * will cause Redis to cache the script, so subsequent calls should then succeed + * using EVALSHA. */ +static void lock_release(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC) +{ + char *cmd, *reply; + int i, cmdlen, replylen; + + /* Keywords, command, and length fallbacks */ + const char *kwd[] = {"EVALSHA", "EVAL"}; + const char *lua[] = {LOCK_RELEASE_SHA_STR, LOCK_RELEASE_LUA_STR}; + int len[] = {LOCK_RELEASE_SHA_LEN, LOCK_RELEASE_LUA_LEN}; + + /* We first want to try EVALSHA and then fall back to EVAL */ + for (i = 0; lock_status->is_locked && i < sizeof(kwd)/sizeof(*kwd); i++) { + /* Construct our command */ + cmdlen = REDIS_SPPRINTF(&cmd, (char*)kwd[i], "sdss", lua[i], len[i], 1, + lock_status->lock_key, strlen(lock_status->lock_key), + lock_status->lock_secret, strlen(lock_status->lock_secret)); + + /* Send it off */ + reply = redis_simple_cmd(redis_sock, cmd, cmdlen, &replylen TSRMLS_CC); + + /* Release lock and cleanup reply if we got one */ + if (reply != NULL) { + lock_status->is_locked = 0; + efree(reply); + } + + /* Cleanup command */ + efree(cmd); + } + + /* Something has failed if we are still locked */ + if (lock_status->is_locked) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to release session lock"); + } +} + /* {{{ PS_OPEN_FUNC */ PS_OPEN_FUNC(redis) @@ -189,7 +418,7 @@ PS_OPEN_FUNC(redis) /* find end of url */ j = i; while (j<path_len && !isspace(save_path[j]) && save_path[j] != ',') - j++; + j++; if (i < j) { int weight = 1; @@ -197,8 +426,8 @@ PS_OPEN_FUNC(redis) int persistent = 0; int database = -1; char *persistent_id = NULL; - zend_string *prefix = NULL, *auth = NULL; long retry_interval = 0; + zend_string *prefix = NULL, *auth = NULL; /* translate unix: into file: */ if (!strncmp(save_path+i, "unix:", sizeof("unix:")-1)) { @@ -308,10 +537,18 @@ PS_CLOSE_FUNC(redis) { redis_pool *pool = PS_GET_MOD_DATA(); - if (pool){ + if (pool) { + redis_pool_member *rpm = redis_pool_get_sock(pool, pool->lock_status.session_key TSRMLS_CC); + + RedisSock *redis_sock = rpm ? rpm->redis_sock : NULL; + if (redis_sock) { + lock_release(redis_sock, &pool->lock_status TSRMLS_CC); + } + redis_pool_free(pool TSRMLS_CC); PS_SET_MOD_DATA(NULL); } + return SUCCESS; } /* }}} */ @@ -338,6 +575,73 @@ redis_session_key(redis_pool_member *rpm, const char *key, int key_len, int *ses return session; } +/* {{{ PS_CREATE_SID_FUNC + */ +PS_CREATE_SID_FUNC(redis) +{ + int retries = 3; + redis_pool *pool = PS_GET_MOD_DATA(); + + if (!pool) { +#if (PHP_MAJOR_VERSION < 7) + return php_session_create_id(NULL, newlen TSRMLS_CC); +#else + return php_session_create_id(NULL TSRMLS_CC); +#endif + } + + while (retries-- > 0) { +#if (PHP_MAJOR_VERSION < 7) + char* sid = php_session_create_id((void **) &pool, newlen TSRMLS_CC); + redis_pool_member *rpm = redis_pool_get_sock(pool, sid TSRMLS_CC); +#else + zend_string* sid = php_session_create_id((void **) &pool TSRMLS_CC); + redis_pool_member *rpm = redis_pool_get_sock(pool, ZSTR_VAL(sid) TSRMLS_CC); +#endif + RedisSock *redis_sock = rpm?rpm->redis_sock:NULL; + + if (!rpm || !redis_sock) { + php_error_docref(NULL TSRMLS_CC, E_NOTICE, + "Redis not available while creating session_id"); + +#if (PHP_MAJOR_VERSION < 7) + efree(sid); + return php_session_create_id(NULL, newlen TSRMLS_CC); +#else + zend_string_release(sid); + return php_session_create_id(NULL TSRMLS_CC); +#endif + } + + int resp_len; +#if (PHP_MAJOR_VERSION < 7) + char *full_session_key = redis_session_key(rpm, sid, strlen(sid), &resp_len); +#else + char *full_session_key = redis_session_key(rpm, ZSTR_VAL(sid), ZSTR_LEN(sid), &resp_len); +#endif + char *full_session_key_nt = estrndup(full_session_key, resp_len); + efree(full_session_key); + pool->lock_status.session_key = full_session_key_nt; + + if (lock_acquire(redis_sock, &pool->lock_status TSRMLS_CC) == SUCCESS) { + return sid; + } + +#if (PHP_MAJOR_VERSION < 7) + efree(sid); +#else + zend_string_release(sid); +#endif + sid = NULL; + } + + php_error_docref(NULL TSRMLS_CC, E_NOTICE, + "Acquiring session lock failed while creating session_id"); + + return NULL; +} +/* }}} */ + /* {{{ PS_READ_FUNC */ PS_READ_FUNC(redis) @@ -363,9 +667,15 @@ PS_READ_FUNC(redis) /* send GET command */ resp = redis_session_key(rpm, skey, skeylen, &resp_len); + pool->lock_status.session_key = estrndup(resp, resp_len); cmd_len = REDIS_SPPRINTF(&cmd, "GET", "s", resp, resp_len); - efree(resp); + + if (lock_acquire(redis_sock, &pool->lock_status TSRMLS_CC) != SUCCESS) { + php_error_docref(NULL TSRMLS_CC, E_NOTICE, + "Acquire of session lock was not successful"); + } + if (redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) { efree(cmd); return FAILURE; @@ -417,17 +727,31 @@ PS_WRITE_FUNC(redis) redis_pool *pool = PS_GET_MOD_DATA(); redis_pool_member *rpm = redis_pool_get_sock(pool, skey TSRMLS_CC); - RedisSock *redis_sock = rpm?rpm->redis_sock:NULL; - if (!rpm || !redis_sock){ + RedisSock *redis_sock = rpm ? rpm->redis_sock : NULL; + if (!redis_sock) { return FAILURE; } /* send SET command */ session = redis_session_key(rpm, skey, skeylen, &session_len); +#if (PHP_MAJOR_VERSION < 7) + /* We need to check for PHP5 if the session key changes (a bug with session_regenerate_id() is causing a missing PS_CREATE_SID call)*/ + int session_key_changed = strlen(pool->lock_status.session_key) != session_len || strncmp(pool->lock_status.session_key, session, session_len) != 0; + if (session_key_changed) { + efree(pool->lock_status.session_key); + pool->lock_status.session_key = estrndup(session, session_len); + } + + if (session_key_changed && lock_acquire(redis_sock, &pool->lock_status TSRMLS_CC) != SUCCESS) { + efree(session); + return FAILURE; + } +#endif cmd_len = REDIS_SPPRINTF(&cmd, "SETEX", "sds", session, session_len, INI_INT("session.gc_maxlifetime"), sval, svallen); efree(session); - if (redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) { + + if (!write_allowed(redis_sock, &pool->lock_status TSRMLS_CC) || redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) { efree(cmd); return FAILURE; } @@ -438,7 +762,7 @@ PS_WRITE_FUNC(redis) return FAILURE; } - if (response_len == 3 && strncmp(response, "+OK", 3) == 0) { + if (IS_REDIS_OK(response, response_len)) { efree(response); return SUCCESS; } else { @@ -469,6 +793,11 @@ PS_DESTROY_FUNC(redis) return FAILURE; } + /* Release lock */ + if (redis_sock) { + lock_release(redis_sock, &pool->lock_status TSRMLS_CC); + } + /* send DEL command */ session = redis_session_key(rpm, skey, skeylen, &session_len); cmd_len = REDIS_SPPRINTF(&cmd, "DEL", "s", session, session_len); diff --git a/redis_session.h b/redis_session.h index 11f861c2..9ab3e5f1 100644 --- a/redis_session.h +++ b/redis_session.h @@ -9,6 +9,7 @@ PS_READ_FUNC(redis); PS_WRITE_FUNC(redis); PS_DESTROY_FUNC(redis); PS_GC_FUNC(redis); +PS_CREATE_SID_FUNC(redis); PS_OPEN_FUNC(rediscluster); PS_CLOSE_FUNC(rediscluster); @@ -19,4 +20,3 @@ PS_GC_FUNC(rediscluster); #endif #endif - diff --git a/tests/RedisClusterTest.php b/tests/RedisClusterTest.php index eb364dbc..f841fa26 100644 --- a/tests/RedisClusterTest.php +++ b/tests/RedisClusterTest.php @@ -23,6 +23,11 @@ class Redis_Cluster_Test extends Redis_Test { RedisCluster::FAILOVER_DISTRIBUTE ); + /** + * @var string + */ + protected $sessionPrefix = 'PHPREDIS_CLUSTER_SESSION:'; + /* Tests we'll skip all together in the context of RedisCluster. The * RedisCluster class doesn't implement specialized (non-redis) commands * such as sortAsc, or sortDesc and other commands such as SELECT are @@ -37,6 +42,21 @@ class Redis_Cluster_Test extends Redis_Test { public function testSwapDB() { return $this->markTestSkipped(); } public function testConnectException() { return $this->markTestSkipped(); } + /* Session locking feature is currently not supported in in context of Redis Cluster. + The biggest issue for this is the distribution nature of Redis cluster */ + public function testSession_savedToRedis() { return $this->markTestSkipped(); } + public function testSession_lockKeyCorrect() { return $this->markTestSkipped(); } + public function testSession_lockingDisabledByDefault() { return $this->markTestSkipped(); } + public function testSession_lockReleasedOnClose() { return $this->markTestSkipped(); } + public function testSession_ttlMaxExecutionTime() { return $this->markTestSkipped(); } + public function testSession_ttlLockExpire() { return $this->markTestSkipped(); } + public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock() { return $this->markTestSkipped(); } + public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock() { return $this->markTestSkipped(); } + public function testSession_correctLockRetryCount() { return $this->markTestSkipped(); } + public function testSession_defaultLockRetryCount() { return $this->markTestSkipped(); } + public function testSession_noUnlockOfOtherProcess() { return $this->markTestSkipped(); } + public function testSession_lockWaitTime() { return $this->markTestSkipped(); } + /* Load our seeds on construction */ public function __construct() { $str_nodemap_file = dirname($_SERVER['PHP_SELF']) . '/nodes/nodemap'; diff --git a/tests/RedisTest.php b/tests/RedisTest.php index 4d807a78..0475f867 100644 --- a/tests/RedisTest.php +++ b/tests/RedisTest.php @@ -21,6 +21,11 @@ class Redis_Test extends TestSuite */ public $redis; + /** + * @var string + */ + protected $sessionPrefix = 'PHPREDIS_SESSION:'; + public function setUp() { $this->redis = $this->newInstance(); $info = $this->redis->info(); @@ -5142,15 +5147,176 @@ class Redis_Test extends TestSuite $this->assertEquals($this->redis->lrange('mylist', 0, -1), Array('A','B','C','D')); } - public function testSession() + public function testSession_savedToRedis() { - ini_set('session.save_handler', 'redis'); - ini_set('session.save_path', 'tcp://localhost:6379'); - if (!@session_start()) { - return $this->markTestSkipped(); - } - session_write_close(); - $this->assertTrue($this->redis->exists('PHPREDIS_SESSION:' . session_id())); + $this->setSessionHandler(); + + $sessionId = $this->generateSessionId(); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false); + + $this->assertTrue($this->redis->exists($this->sessionPrefix . $sessionId)); + $this->assertTrue($sessionSuccessful); + } + + public function testSession_lockKeyCorrect() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 5, true); + usleep(100000); + + $this->assertTrue($this->redis->exists($this->sessionPrefix . $sessionId . '_LOCK')); + } + + public function testSession_lockingDisabledByDefault() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 5, true, 300, false); + usleep(100000); + + $start = microtime(true); + $sessionSuccessful = $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 300, false); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertFalse($this->redis->exists($this->sessionPrefix . $sessionId . '_LOCK')); + $this->assertTrue($elapsedTime < 1); + $this->assertTrue($sessionSuccessful); + } + + public function testSession_lockReleasedOnClose() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 1, true); + usleep(1100000); + + $this->assertFalse($this->redis->exists($this->sessionPrefix . $sessionId . '_LOCK')); + } + + public function testSession_ttlMaxExecutionTime() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 10, true, 2); + usleep(100000); + + $start = microtime(true); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertTrue($elapsedTime < 3); + $this->assertTrue($sessionSuccessful); + } + + public function testSession_ttlLockExpire() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 10, true, 300, true, null, -1, 2); + usleep(100000); + + $start = microtime(true); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertTrue($elapsedTime < 3); + $this->assertTrue($sessionSuccessful); + } + + public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 2, true, 300, true, null, -1, 1, 'firstProcess'); + usleep(1500000); // 1.5 sec + $writeSuccessful = $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 10, 'secondProcess'); + sleep(1); + + $this->assertTrue($writeSuccessful); + $this->assertEquals('secondProcess', $this->getSessionData($sessionId)); + } + + public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $writeSuccessful = $this->startSessionProcess($sessionId, 2, false, 300, true, null, -1, 1, 'firstProcess'); + + $this->assertFalse($writeSuccessful); + $this->assertTrue('firstProcess' !== $this->getSessionData($sessionId)); + } + + public function testSession_correctLockRetryCount() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 10, true); + usleep(100000); + + $start = microtime(true); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 10, true, 1000000, 3); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertTrue($elapsedTime > 3 && $elapsedTime < 4); + $this->assertFalse($sessionSuccessful); + } + + public function testSession_defaultLockRetryCount() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 10, true); + usleep(100000); + + $start = microtime(true); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 10, true, 200000, 0); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertTrue($elapsedTime > 2 && $elapsedTime < 3); + $this->assertFalse($sessionSuccessful); + } + + public function testSession_noUnlockOfOtherProcess() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 3, true, 1); // Process 1 + usleep(100000); + $this->startSessionProcess($sessionId, 5, true); // Process 2 + + $start = microtime(true); + // Waiting until TTL of process 1 ended and process 2 locked the session, + // because is not guaranteed which waiting process gets the next lock + sleep(1); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertTrue($elapsedTime > 5); + $this->assertTrue($sessionSuccessful); + } + + public function testSession_lockWaitTime() + { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 1, true, 300); + usleep(100000); + + $start = microtime(true); + $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 300, true, 3000000); + $end = microtime(true); + $elapsedTime = $end - $start; + + $this->assertTrue($elapsedTime > 2.5); + $this->assertTrue($elapsedTime < 3.5); + $this->assertTrue($sessionSuccessful); } public function testMultipleConnect() { @@ -5175,5 +5341,199 @@ class Redis_Test extends TestSuite $this->assertTrue(strpos($e, "timed out") !== false); } } + + public function testSession_regenerateSessionId_noLock_noDestroy() { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_noLock_withDestroy() { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, false, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_withLock_noDestroy() { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_withLock_withDestroy() { + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, true, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_noLock_noDestroy_withProxy() { + if (!interface_exists('SessionHandlerInterface')) { + $this->markTestSkipped('session handler interface not available in PHP < 5.4'); + } + + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, false, false, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_noLock_withDestroy_withProxy() { + if (!interface_exists('SessionHandlerInterface')) { + $this->markTestSkipped('session handler interface not available in PHP < 5.4'); + } + + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, false, true, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_withLock_noDestroy_withProxy() { + if (!interface_exists('SessionHandlerInterface')) { + $this->markTestSkipped('session handler interface not available in PHP < 5.4'); + } + + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, true, false, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + public function testSession_regenerateSessionId_withLock_withDestroy_withProxy() { + if (!interface_exists('SessionHandlerInterface')) { + $this->markTestSkipped('session handler interface not available in PHP < 5.4'); + } + + $this->setSessionHandler(); + $sessionId = $this->generateSessionId(); + $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar'); + + $newSessionId = $this->regenerateSessionId($sessionId, true, true, true); + + $this->assertTrue($newSessionId !== $sessionId); + $this->assertEquals('bar', $this->getSessionData($newSessionId)); + } + + private function setSessionHandler() + { + $host = $this->getHost() ?: 'localhost'; + + ini_set('session.save_handler', 'redis'); + ini_set('session.save_path', 'tcp://' . $host . ':6379'); + } + + /** + * @return string + */ + private function generateSessionId() + { + if (function_exists('session_create_id')) { + return session_create_id(); + } else { + $encoded = bin2hex(openssl_random_pseudo_bytes(8)); + return $encoded; + } + } + + /** + * @param string $sessionId + * @param int $sleepTime + * @param bool $background + * @param int $maxExecutionTime + * @param bool $locking_enabled + * @param int $lock_wait_time + * @param int $lock_retries + * @param int $lock_expires + * @param string $sessionData + * + * @return bool + */ + private function startSessionProcess($sessionId, $sleepTime, $background, $maxExecutionTime = 300, $locking_enabled = true, $lock_wait_time = null, $lock_retries = -1, $lock_expires = 0, $sessionData = '') + { + if (substr(php_uname(), 0, 7) == "Windows"){ + $this->markTestSkipped(); + return true; + } else { + $commandParameters = array($this->getHost(), $sessionId, $sleepTime, $maxExecutionTime, $lock_retries, $lock_expires, $sessionData); + if ($locking_enabled) { + $commandParameters[] = '1'; + + if ($lock_wait_time != null) { + $commandParameters[] = $lock_wait_time; + } + } + $commandParameters = array_map('escapeshellarg', $commandParameters); + + $command = 'php ' . __DIR__ . '/startSession.php ' . implode(' ', $commandParameters); + $command .= $background ? ' 2>/dev/null > /dev/null &' : ' 2>&1'; + + exec($command, $output); + return ($background || (count($output) == 1 && $output[0] == 'SUCCESS')) ? true : false; + } + } + + /** + * @param string $sessionId + * + * @return string + */ + private function getSessionData($sessionId) + { + $command = 'php ' . __DIR__ . '/getSessionData.php ' . escapeshellarg($this->getHost()) . ' ' . escapeshellarg($sessionId); + exec($command, $output); + + return $output[0]; + } + + /** + * @param string $sessionId + * @param bool $locking + * @param bool $destroyPrevious + * @param bool $sessionProxy + * + * @return string + */ + private function regenerateSessionId($sessionId, $locking = false, $destroyPrevious = false, $sessionProxy = false) + { + $args = array_map('escapeshellarg', array($sessionId, $locking, $destroyPrevious, $sessionProxy)); + + $command = 'php --no-php-ini --define extension=igbinary.so --define extension=' . __DIR__ . '/../modules/redis.so ' . __DIR__ . '/regenerateSessionId.php ' . escapeshellarg($this->getHost()) . ' ' . implode(' ', $args); + + exec($command, $output); + + return $output[0]; + } } ?> diff --git a/tests/getSessionData.php b/tests/getSessionData.php new file mode 100644 index 00000000..b5bea74a --- /dev/null +++ b/tests/getSessionData.php @@ -0,0 +1,19 @@ +<?php +error_reporting(E_ERROR | E_WARNING); + +$redisHost = $argv[1]; +$sessionId = $argv[2]; + +if (empty($redisHost)) { + $redisHost = 'localhost'; +} + +ini_set('session.save_handler', 'redis'); +ini_set('session.save_path', 'tcp://' . $redisHost . ':6379'); + +session_id($sessionId); +if (!session_start()) { + echo "session_start() was nut successful"; +} else { + echo isset($_SESSION['redis_test']) ? $_SESSION['redis_test'] : 'Key redis_test not found'; +} diff --git a/tests/regenerateSessionId.php b/tests/regenerateSessionId.php new file mode 100644 index 00000000..6d49c0ab --- /dev/null +++ b/tests/regenerateSessionId.php @@ -0,0 +1,83 @@ +<?php +error_reporting(E_ERROR | E_WARNING); + +$redisHost = $argv[1]; +$sessionId = $argv[2]; +$locking = !!$argv[3]; +$destroyPrevious = !!$argv[4]; +$sessionProxy = !!$argv[5]; + +if (empty($redisHost)) { + $redisHost = 'localhost'; +} + +ini_set('session.save_handler', 'redis'); +ini_set('session.save_path', 'tcp://' . $redisHost . ':6379'); + +if ($locking) { + ini_set('redis.session.locking_enabled', true); +} + +if (interface_exists('SessionHandlerInterface')) { + class TestHandler implements SessionHandlerInterface + { + /** + * @var SessionHandler + */ + private $handler; + + public function __construct() + { + $this->handler = new SessionHandler(); + } + + public function close() + { + return $this->handler->close(); + } + + public function destroy($session_id) + { + return $this->handler->destroy($session_id); + } + + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } + + public function open($save_path, $name) + { + return $this->handler->open($save_path, $name); + } + + public function read($session_id) + { + return $this->handler->read($session_id); + } + + public function write($session_id, $session_data) + { + return $this->handler->write($session_id, $session_data); + } + } +} + +if ($sessionProxy) { + $handler = new TestHandler(); + session_set_save_handler($handler); +} + +session_id($sessionId); +if (!session_start()) { + $result = "FAILED: session_start()"; +} +elseif (!session_regenerate_id($destroyPrevious)) { + $result = "FAILED: session_regenerate_id()"; +} +else { + $result = session_id(); +} +session_write_close(); +echo $result; + diff --git a/tests/startSession.php b/tests/startSession.php new file mode 100644 index 00000000..081a6951 --- /dev/null +++ b/tests/startSession.php @@ -0,0 +1,38 @@ +<?php +error_reporting(E_ERROR | E_WARNING); + +$redisHost = $argv[1]; +$sessionId = $argv[2]; +$sleepTime = $argv[3]; +$maxExecutionTime = $argv[4]; +$lock_retries = $argv[5]; +$lock_expire = $argv[6]; +$sessionData = $argv[7]; + +if (empty($redisHost)) { + $redisHost = 'localhost'; +} + +ini_set('session.save_handler', 'redis'); +ini_set('session.save_path', 'tcp://' . $redisHost . ':6379'); +ini_set('max_execution_time', $maxExecutionTime); +ini_set('redis.session.lock_retries', $lock_retries); +ini_set('redis.session.lock_expire', $lock_expire); + +if (isset($argv[8])) { + ini_set('redis.session.locking_enabled', $argv[8]); +} + +if (isset($argv[9])) { + ini_set('redis.session.lock_wait_time', $argv[9]); +} + +session_id($sessionId); +$sessionStartSuccessful = session_start(); +sleep($sleepTime); +if (!empty($sessionData)) { + $_SESSION['redis_test'] = $sessionData; +} +session_write_close(); + +echo $sessionStartSuccessful ? 'SUCCESS' : 'FAILURE';
\ No newline at end of file |