blob: ef18defe2e486c3920a72b910bdd8a998789fb2f [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 {
138 $session_data = (string) $this->_redis->get($this->_key_prefix.$session_id);
139 $this->_fingerprint = md5($session_data);
140 return $session_data;
141 }
142
143 return FALSE;
144 }
145
146 public function write($session_id, $session_data)
147 {
148 if (isset($this->_redis, $this->_lock_key))
149 {
Andrey Andreev2a1f9402014-08-27 23:52:55 +0300150 $this->_redis->setTimeout($this->_lock_key, 5);
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300151 if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
152 {
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300153 if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300154 {
155 $this->_fingerprint = $fingerprint;
156 return TRUE;
157 }
158
159 return FALSE;
160 }
161
Andrey Andreevdfb39be2014-10-06 01:50:14 +0300162 return $this->_redis->setTimeout($this->_key_prefix.$session_id, $this->_config['expiration']);
Andrey Andreev43f6cdb2014-08-27 22:26:40 +0300163 }
164
165 return FALSE;
166 }
167
168 // ------------------------------------------------------------------------
169
170 public function close()
171 {
172 if (isset($this->_redis))
173 {
174 try {
175 if ($this->_redis->ping() === '+PONG')
176 {
177 isset($this->_lock_key) && $this->_redis->delete($this->_lock_key);
178 if ( ! $this->_redis->close())
179 {
180 return FALSE;
181 }
182 }
183 }
184 catch (RedisException $e)
185 {
186 log_message('error', 'Session: Got RedisException on close(): '.$e->getMessage());
187 }
188
189 $this->_redis = NULL;
190 return TRUE;
191 }
192
193 return FALSE;
194 }
195
196 // ------------------------------------------------------------------------
197
198 public function destroy($session_id)
199 {
200 if (isset($this->_redis, $this->_lock_key))
201 {
202 if ($this->_redis->delete($this->_key_prefix.$session_id) !== 1)
203 {
204 log_message('debug', 'Session: Redis::delete() expected to return 1, got '.var_export($result, TRUE).' instead.');
205 }
206
207 return ($this->_cookie_destroy() && $this->close());
208 }
209
210 return $this->close();
211 }
212
213 // ------------------------------------------------------------------------
214
215 public function gc($maxlifetime)
216 {
217 // TODO: keys()/getKeys() is said to be performance-intensive,
218 // although it supports patterns (*, [charlist] at the very least).
219 // scan() seems to be recommended, but requires redis 2.8
220 // Not sure if we need any of these though, as we set keys with expire times
221 return TRUE;
222 }
223
224 // ------------------------------------------------------------------------
225
226 protected function _get_lock($session_id)
227 {
228 if (isset($this->_lock_key))
229 {
230 return $this->_redis->setTimeout($this->_lock_key, 5);
231 }
232
233 $lock_key = $this->_key_prefix.$session_id.':lock';
234 if (($ttl = $this->_redis->ttl($lock_key)) < 1)
235 {
236 if ( ! $this->_redis->setex($lock_key, 5, time()))
237 {
238 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
239 return FALSE;
240 }
241
242 $this->_lock_key = $lock_key;
243
244 if ($ttl === -1)
245 {
246 log_message('debug', 'Session: Lock for '.$this->_key_prefix.$session_id.' had no TTL, overriding.');
247 }
248
249 $this->_lock = TRUE;
250 return TRUE;
251 }
252
253 // Another process has the lock, we'll try to wait for it to free itself ...
254 $attempt = 0;
255 while ($attempt++ < 5)
256 {
257 usleep(($ttl * 1000000) - 20000);
258 if (($ttl = $this->_redis->ttl($lock_key)) > 0)
259 {
260 continue;
261 }
262
263 if ( ! $this->_redis->setex($lock_key, 5, time()))
264 {
265 log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
266 return FALSE;
267 }
268
269 $this->_lock_key = $lock_key;
270 break;
271 }
272
273 if ($attempt === 5)
274 {
275 log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 5 attempts, aborting.');
276 return FALSE;
277 }
278
279 $this->_lock = TRUE;
280 return TRUE;
281 }
282
283 // ------------------------------------------------------------------------
284
285 protected function _release_lock()
286 {
287 if (isset($this->_redis, $this->_lock_key) && $this->_lock)
288 {
289 if ( ! $this->_redis->delete($this->_lock_key))
290 {
291 log_message('error', 'Session: Error while trying to free lock for '.$this->_key_prefix.$session_id);
292 return FALSE;
293 }
294
295 $this->_lock_key = NULL;
296 $this->_lock = FALSE;
297 }
298
299 return TRUE;
300 }
301
302}
303
304/* End of file Session_redis_driver.php */
305/* Location: ./system/libraries/Session/drivers/Session_redis_driver.php */