blob: bc6150d2dea8845c4d2accd912bc31632ad3eb80 [file] [log] [blame]
Andrey Andreev43f6cdb2014-08-27 22:26:40 +03001<?php
2/**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP 5.2.4 or newer
6 *
7 * NOTICE OF LICENSE
8 *
9 * Licensed under the Open Software License version 3.0
10 *
11 * This source file is subject to the Open Software License (OSL 3.0) that is
12 * bundled with this package in the files license.txt / license.rst. It is
13 * also available through the world wide web at this URL:
14 * http://opensource.org/licenses/OSL-3.0
15 * If you did not receive a copy of the license and are unable to obtain it
16 * through the world wide web, please send an email to
17 * licensing@ellislab.com so we can send you a copy immediately.
18 *
19 * @package CodeIgniter
20 * @author Andrey Andreev
21 * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
22 * @license http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
23 * @link http://codeigniter.com
24 * @since Version 3.0
25 * @filesource
26 */
27defined('BASEPATH') OR exit('No direct script access allowed');
28
29/**
30 * CodeIgniter Session Redis Driver
31 *
32 * @package CodeIgniter
33 * @subpackage Libraries
34 * @category Sessions
35 * @author Andrey Andreev
36 * @link http://codeigniter.com/user_guide/libraries/sessions.html
37 */
38class CI_Session_redis_driver extends CI_Session_driver implements SessionHandlerInterface {
39
40 /**
Andrey Andreev43f6cdb2014-08-27 22:26:40 +030041 * phpRedis instance
42 *
43 * @var resource
44 */
45 protected $_redis;
46
47 /**
48 * Key prefix
49 *
50 * @var string
51 */
52 protected $_key_prefix = 'ci_session:';
53
54 /**
55 * Lock key
56 *
57 * @var string
58 */
59 protected $_lock_key;
60
61 // ------------------------------------------------------------------------
62
63 /**
64 * Class constructor
65 *
66 * @param array $params Configuration parameters
67 * @return void
68 */
69 public function __construct(&$params)
70 {
71 parent::__construct($params);
72
Andrey Andreevdfb39be2014-10-06 01:50:14 +030073 if (empty($this->_config['save_path']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +030074 {
75 log_message('error', 'Session: No Redis save path configured.');
76 }
Andrey Andreevdfb39be2014-10-06 01:50:14 +030077 elseif (preg_match('#(?:tcp://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->_config['save_path'], $matches))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +030078 {
Andrey Andreev39ec2952014-09-17 14:16:05 +030079 isset($matches[3]) OR $matches[3] = ''; // Just to avoid undefined index notices below
Andrey Andreevdfb39be2014-10-06 01:50:14 +030080 $this->_config['save_path'] = array(
Andrey Andreev43f6cdb2014-08-27 22:26:40 +030081 'host' => $matches[1],
82 'port' => empty($matches[2]) ? NULL : $matches[2],
83 'password' => preg_match('#auth=([^\s&]+)#', $matches[3], $match) ? $match[1] : NULL,
84 'database' => preg_match('#database=(\d+)#', $matches[3], $match) ? (int) $match[1] : NULL,
85 'timeout' => preg_match('#timeout=(\d+\.\d+)#', $matches[3], $match) ? (float) $match[1] : NULL
86 );
87
88 preg_match('#prefix=([^\s&]+)#', $matches[3], $match) && $this->_key_prefix = $match[1];
89 }
90 else
91 {
Andrey Andreevdfb39be2014-10-06 01:50:14 +030092 log_message('error', 'Session: Invalid Redis save path format: '.$this->_config['save_path']);
Andrey Andreev43f6cdb2014-08-27 22:26:40 +030093 }
94
Andrey Andreevdfb39be2014-10-06 01:50:14 +030095 if ($this->_config['match_ip'] === TRUE)
Andrey Andreev43f6cdb2014-08-27 22:26:40 +030096 {
97 $this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
98 }
99 }
100
101 // ------------------------------------------------------------------------
102
103 public function open($save_path, $name)
104 {
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300105 if (empty($this->_config['save_path']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300106 {
107 return FALSE;
108 }
109
110 $redis = new Redis();
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300111 if ( ! $redis->connect($this->_config['save_path']['host'], $this->_config['save_path']['port'], $this->_config['save_path']['timeout']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300112 {
113 log_message('error', 'Session: Unable to connect to Redis with the configured settings.');
114 }
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300115 elseif (isset($this->_config['save_path']['password']) && ! $redis->auth($this->_config['save_path']['password']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300116 {
117 log_message('error', 'Session: Unable to authenticate to Redis instance.');
118 }
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300119 elseif (isset($this->_config['save_path']['database']) && ! $redis->select($this->_config['save_path']['database']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300120 {
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300121 log_message('error', 'Session: Unable to select Redis database with index '.$this->_config['save_path']['database']);
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300122 }
123 else
124 {
125 $this->_redis = $redis;
126 return TRUE;
127 }
128
129 return FALSE;
130 }
131
132 // ------------------------------------------------------------------------
133
134 public function read($session_id)
135 {
136 if (isset($this->_redis) && $this->_get_lock($session_id))
137 {
Andrey Andreev7474a672014-10-31 23:35:32 +0200138 // Needed by write() to detect session_regenerate_id() calls
139 $this->_session_id = $session_id;
140
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300141 $session_data = (string) $this->_redis->get($this->_key_prefix.$session_id);
142 $this->_fingerprint = md5($session_data);
143 return $session_data;
144 }
145
146 return FALSE;
147 }
148
149 public function write($session_id, $session_data)
150 {
Andrey Andreev7474a672014-10-31 23:35:32 +0200151 if ( ! isset($this->_redis))
152 {
153 return FALSE;
154 }
155 // Was the ID regenerated?
156 elseif ($session_id !== $this->_session_id)
157 {
158 if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
159 {
160 return FALSE;
161 }
162
163 $this->_fingerprint = md5('');
164 $this->_session_id = $session_id;
165 }
166
167 if (isset($this->_lock_key))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300168 {
Andrey Andreev2a1f9402014-08-27 23:52:55 +0300169 $this->_redis->setTimeout($this->_lock_key, 5);
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300170 if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
171 {
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300172 if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300173 {
174 $this->_fingerprint = $fingerprint;
175 return TRUE;
176 }
177
178 return FALSE;
179 }
180
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300181 return $this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_config['expiration']);
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300182 }
183
184 return FALSE;
185 }
186
187 // ------------------------------------------------------------------------
188
189 public function close()
190 {
191 if (isset($this->_redis))
192 {
193 try {
194 if ($this->_redis->ping() === '+PONG')
195 {
196 isset($this->_lock_key) && $this->_redis->delete($this->_lock_key);
197 if ( ! $this->_redis->close())
198 {
199 return FALSE;
200 }
201 }
202 }
203 catch (RedisException $e)
204 {
205 log_message('error', 'Session: Got RedisException on close(): '.$e->getMessage());
206 }
207
208 $this->_redis = NULL;
209 return TRUE;
210 }
211
Andrey Andreev7474a672014-10-31 23:35:32 +0200212 return TRUE;
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300213 }
214
215 // ------------------------------------------------------------------------
216
217 public function destroy($session_id)
218 {
219 if (isset($this->_redis, $this->_lock_key))
220 {
221 if ($this->_redis->delete($this->_key_prefix.$session_id) !== 1)
222 {
223 log_message('debug', 'Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.');
224 }
225
Andrey Andreev7474a672014-10-31 23:35:32 +0200226 return $this->_cookie_destroy();
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300227 }
228
Andrey Andreev7474a672014-10-31 23:35:32 +0200229 return FALSE;
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300230 }
231
232 // ------------------------------------------------------------------------
233
234 public function gc($maxlifetime)
235 {
Andrey Andreev7474a672014-10-31 23:35:32 +0200236 // Not necessary, Redis takes care of that.
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300237 return TRUE;
238 }
239
240 // ------------------------------------------------------------------------
241
242 protected function _get_lock($session_id)
243 {
244 if (isset($this->_lock_key))
245 {
246 return $this->_redis->setTimeout($this->_lock_key, 5);
247 }
248
249 $lock_key = $this->_key_prefix.$session_id.':lock';
250 if (($ttl = $this->_redis->ttl($lock_key)) < 1)
251 {
252 if ( ! $this->_redis->setex($lock_key, 5, time()))
253 {
254 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
255 return FALSE;
256 }
257
258 $this->_lock_key = $lock_key;
259
260 if ($ttl === -1)
261 {
262 log_message('debug', 'Session: Lock for '.$this->_key_prefix.$session_id.' had no TTL, overriding.');
263 }
264
265 $this->_lock = TRUE;
266 return TRUE;
267 }
268
269 // Another process has the lock, we'll try to wait for it to free itself ...
270 $attempt = 0;
271 while ($attempt++ < 5)
272 {
273 usleep(($ttl * 1000000) - 20000);
274 if (($ttl = $this->_redis->ttl($lock_key)) > 0)
275 {
276 continue;
277 }
278
279 if ( ! $this->_redis->setex($lock_key, 5, time()))
280 {
281 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
282 return FALSE;
283 }
284
285 $this->_lock_key = $lock_key;
286 break;
287 }
288
289 if ($attempt === 5)
290 {
291 log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 5 attempts, aborting.');
292 return FALSE;
293 }
294
295 $this->_lock = TRUE;
296 return TRUE;
297 }
298
299 // ------------------------------------------------------------------------
300
301 protected function _release_lock()
302 {
303 if (isset($this->_redis, $this->_lock_key) && $this->_lock)
304 {
305 if ( ! $this->_redis->delete($this->_lock_key))
306 {
307 log_message('error', 'Session: Error while trying to free lock for '.$this->_key_prefix.$session_id);
308 return FALSE;
309 }
310
311 $this->_lock_key = NULL;
312 $this->_lock = FALSE;
313 }
314
315 return TRUE;
316 }
317
318}
319
320/* End of file Session_redis_driver.php */
321/* Location: ./system/libraries/Session/drivers/Session_redis_driver.php */