Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/phpredis/phpredis.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--php_redis.h5
-rw-r--r--redis.c10
-rw-r--r--redis_session.c349
-rw-r--r--redis_session.h2
-rw-r--r--tests/RedisClusterTest.php20
-rw-r--r--tests/RedisTest.php376
-rw-r--r--tests/getSessionData.php19
-rw-r--r--tests/regenerateSessionId.php83
-rw-r--r--tests/startSession.php38
10 files changed, 875 insertions, 28 deletions
diff --git a/.gitignore b/.gitignore
index 046241a9..0462ff37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/redis.c b/redis.c
index 4c645ca6..79640f2d 100644
--- a/redis.c
+++ b/redis.c
@@ -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