Subversion-Projekte lars-tiefland.ci

Revision

Revision 2257 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
68 lars 1
<?php
2
/**
3
 * CodeIgniter
4
 *
5
 * An open source application development framework for PHP
6
 *
7
 * This content is released under the MIT License (MIT)
8
 *
2414 lars 9
 * Copyright (c) 2014 - 2019, British Columbia Institute of Technology
68 lars 10
 *
11
 * Permission is hereby granted, free of charge, to any person obtaining a copy
12
 * of this software and associated documentation files (the "Software"), to deal
13
 * in the Software without restriction, including without limitation the rights
14
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
 * copies of the Software, and to permit persons to whom the Software is
16
 * furnished to do so, subject to the following conditions:
17
 *
18
 * The above copyright notice and this permission notice shall be included in
19
 * all copies or substantial portions of the Software.
20
 *
21
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
 * THE SOFTWARE.
28
 *
29
 * @package	CodeIgniter
30
 * @author	EllisLab Dev Team
31
 * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
2414 lars 32
 * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
33
 * @license	https://opensource.org/licenses/MIT	MIT License
68 lars 34
 * @link	https://codeigniter.com
35
 * @since	Version 3.0.0
36
 * @filesource
37
 */
38
defined('BASEPATH') OR exit('No direct script access allowed');
39
 
40
/**
41
 * CodeIgniter Session Memcached Driver
42
 *
43
 * @package	CodeIgniter
44
 * @subpackage	Libraries
45
 * @category	Sessions
46
 * @author	Andrey Andreev
47
 * @link	https://codeigniter.com/user_guide/libraries/sessions.html
48
 */
49
class CI_Session_memcached_driver extends CI_Session_driver implements SessionHandlerInterface {
50
 
51
	/**
52
	 * Memcached instance
53
	 *
54
	 * @var	Memcached
55
	 */
56
	protected $_memcached;
57
 
58
	/**
59
	 * Key prefix
60
	 *
61
	 * @var	string
62
	 */
63
	protected $_key_prefix = 'ci_session:';
64
 
65
	/**
66
	 * Lock key
67
	 *
68
	 * @var	string
69
	 */
70
	protected $_lock_key;
71
 
72
	// ------------------------------------------------------------------------
73
 
74
	/**
75
	 * Class constructor
76
	 *
77
	 * @param	array	$params	Configuration parameters
78
	 * @return	void
79
	 */
80
	public function __construct(&$params)
81
	{
82
		parent::__construct($params);
83
 
84
		if (empty($this->_config['save_path']))
85
		{
86
			log_message('error', 'Session: No Memcached save path configured.');
87
		}
88
 
89
		if ($this->_config['match_ip'] === TRUE)
90
		{
91
			$this->_key_prefix .= $_SERVER['REMOTE_ADDR'].':';
92
		}
93
	}
94
 
95
	// ------------------------------------------------------------------------
96
 
97
	/**
98
	 * Open
99
	 *
100
	 * Sanitizes save_path and initializes connections.
101
	 *
102
	 * @param	string	$save_path	Server path(s)
103
	 * @param	string	$name		Session cookie name, unused
104
	 * @return	bool
105
	 */
106
	public function open($save_path, $name)
107
	{
108
		$this->_memcached = new Memcached();
109
		$this->_memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, TRUE); // required for touch() usage
110
		$server_list = array();
111
		foreach ($this->_memcached->getServerList() as $server)
112
		{
113
			$server_list[] = $server['host'].':'.$server['port'];
114
		}
115
 
116
		if ( ! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->_config['save_path'], $matches, PREG_SET_ORDER))
117
		{
118
			$this->_memcached = NULL;
119
			log_message('error', 'Session: Invalid Memcached save path format: '.$this->_config['save_path']);
2414 lars 120
			return $this->_failure;
68 lars 121
		}
122
 
123
		foreach ($matches as $match)
124
		{
125
			// If Memcached already has this server (or if the port is invalid), skip it
126
			if (in_array($match[1].':'.$match[2], $server_list, TRUE))
127
			{
128
				log_message('debug', 'Session: Memcached server pool already has '.$match[1].':'.$match[2]);
129
				continue;
130
			}
131
 
132
			if ( ! $this->_memcached->addServer($match[1], $match[2], isset($match[3]) ? $match[3] : 0))
133
			{
134
				log_message('error', 'Could not add '.$match[1].':'.$match[2].' to Memcached server pool.');
135
			}
136
			else
137
			{
138
				$server_list[] = $match[1].':'.$match[2];
139
			}
140
		}
141
 
142
		if (empty($server_list))
143
		{
144
			log_message('error', 'Session: Memcached server pool is empty.');
2414 lars 145
			return $this->_failure;
68 lars 146
		}
147
 
2414 lars 148
		$this->php5_validate_id();
149
 
68 lars 150
		return $this->_success;
151
	}
152
 
153
	// ------------------------------------------------------------------------
154
 
155
	/**
156
	 * Read
157
	 *
158
	 * Reads session data and acquires a lock
159
	 *
160
	 * @param	string	$session_id	Session ID
161
	 * @return	string	Serialized session data
162
	 */
163
	public function read($session_id)
164
	{
165
		if (isset($this->_memcached) && $this->_get_lock($session_id))
166
		{
167
			// Needed by write() to detect session_regenerate_id() calls
168
			$this->_session_id = $session_id;
169
 
170
			$session_data = (string) $this->_memcached->get($this->_key_prefix.$session_id);
171
			$this->_fingerprint = md5($session_data);
172
			return $session_data;
173
		}
174
 
2414 lars 175
		return $this->_failure;
68 lars 176
	}
177
 
178
	// ------------------------------------------------------------------------
179
 
180
	/**
181
	 * Write
182
	 *
183
	 * Writes (create / update) session data
184
	 *
185
	 * @param	string	$session_id	Session ID
186
	 * @param	string	$session_data	Serialized session data
187
	 * @return	bool
188
	 */
189
	public function write($session_id, $session_data)
190
	{
2049 lars 191
		if ( ! isset($this->_memcached, $this->_lock_key))
68 lars 192
		{
2414 lars 193
			return $this->_failure;
68 lars 194
		}
195
		// Was the ID regenerated?
196
		elseif ($session_id !== $this->_session_id)
197
		{
198
			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
199
			{
2414 lars 200
				return $this->_failure;
68 lars 201
			}
202
 
203
			$this->_fingerprint = md5('');
204
			$this->_session_id = $session_id;
205
		}
206
 
2049 lars 207
		$key = $this->_key_prefix.$session_id;
208
 
209
		$this->_memcached->replace($this->_lock_key, time(), 300);
210
		if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
68 lars 211
		{
2049 lars 212
			if ($this->_memcached->set($key, $session_data, $this->_config['expiration']))
68 lars 213
			{
2049 lars 214
				$this->_fingerprint = $fingerprint;
68 lars 215
				return $this->_success;
216
			}
2049 lars 217
 
2414 lars 218
			return $this->_failure;
68 lars 219
		}
2049 lars 220
		elseif (
221
			$this->_memcached->touch($key, $this->_config['expiration'])
222
			OR ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND && $this->_memcached->set($key, $session_data, $this->_config['expiration']))
223
		)
224
		{
225
			return $this->_success;
226
		}
68 lars 227
 
2414 lars 228
		return $this->_failure;
68 lars 229
	}
230
 
231
	// ------------------------------------------------------------------------
232
 
233
	/**
234
	 * Close
235
	 *
236
	 * Releases locks and closes connection.
237
	 *
238
	 * @return	bool
239
	 */
240
	public function close()
241
	{
242
		if (isset($this->_memcached))
243
		{
244
			$this->_release_lock();
245
			if ( ! $this->_memcached->quit())
246
			{
2414 lars 247
				return $this->_failure;
68 lars 248
			}
249
 
250
			$this->_memcached = NULL;
251
			return $this->_success;
252
		}
253
 
2414 lars 254
		return $this->_failure;
68 lars 255
	}
256
 
257
	// ------------------------------------------------------------------------
258
 
259
	/**
260
	 * Destroy
261
	 *
262
	 * Destroys the current session.
263
	 *
264
	 * @param	string	$session_id	Session ID
265
	 * @return	bool
266
	 */
267
	public function destroy($session_id)
268
	{
269
		if (isset($this->_memcached, $this->_lock_key))
270
		{
271
			$this->_memcached->delete($this->_key_prefix.$session_id);
272
			$this->_cookie_destroy();
273
			return $this->_success;
274
		}
275
 
2414 lars 276
		return $this->_failure;
68 lars 277
	}
278
 
279
	// ------------------------------------------------------------------------
280
 
281
	/**
282
	 * Garbage Collector
283
	 *
284
	 * Deletes expired sessions
285
	 *
286
	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
287
	 * @return	bool
288
	 */
289
	public function gc($maxlifetime)
290
	{
291
		// Not necessary, Memcached takes care of that.
292
		return $this->_success;
293
	}
294
 
2414 lars 295
	// --------------------------------------------------------------------
296
 
297
	/**
298
	 * Validate ID
299
	 *
300
	 * Checks whether a session ID record exists server-side,
301
	 * to enforce session.use_strict_mode.
302
	 *
303
	 * @param	string	$id
304
	 * @return	bool
305
	 */
306
	public function validateSessionId($id)
307
	{
308
		$this->_memcached->get($this->_key_prefix.$id);
309
		return ($this->_memcached->getResultCode() === Memcached::RES_SUCCESS);
310
	}
311
 
68 lars 312
	// ------------------------------------------------------------------------
313
 
314
	/**
315
	 * Get lock
316
	 *
317
	 * Acquires an (emulated) lock.
318
	 *
319
	 * @param	string	$session_id	Session ID
320
	 * @return	bool
321
	 */
322
	protected function _get_lock($session_id)
323
	{
324
		// PHP 7 reuses the SessionHandler object on regeneration,
325
		// so we need to check here if the lock key is for the
326
		// correct session ID.
327
		if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
328
		{
329
			if ( ! $this->_memcached->replace($this->_lock_key, time(), 300))
330
			{
331
				return ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND)
2107 lars 332
					? $this->_memcached->add($this->_lock_key, time(), 300)
68 lars 333
					: FALSE;
334
			}
2257 lars 335
 
336
			return TRUE;
68 lars 337
		}
338
 
339
		// 30 attempts to obtain a lock, in case another request already has it
340
		$lock_key = $this->_key_prefix.$session_id.':lock';
341
		$attempt = 0;
342
		do
343
		{
344
			if ($this->_memcached->get($lock_key))
345
			{
346
				sleep(1);
347
				continue;
348
			}
349
 
2107 lars 350
			$method = ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND) ? 'add' : 'set';
351
			if ( ! $this->_memcached->$method($lock_key, time(), 300))
68 lars 352
			{
353
				log_message('error', 'Session: Error while trying to obtain lock for '.$this->_key_prefix.$session_id);
354
				return FALSE;
355
			}
356
 
357
			$this->_lock_key = $lock_key;
358
			break;
359
		}
360
		while (++$attempt < 30);
361
 
362
		if ($attempt === 30)
363
		{
364
			log_message('error', 'Session: Unable to obtain lock for '.$this->_key_prefix.$session_id.' after 30 attempts, aborting.');
365
			return FALSE;
366
		}
367
 
368
		$this->_lock = TRUE;
369
		return TRUE;
370
	}
371
 
372
	// ------------------------------------------------------------------------
373
 
374
	/**
375
	 * Release lock
376
	 *
377
	 * Releases a previously acquired lock
378
	 *
379
	 * @return	bool
380
	 */
381
	protected function _release_lock()
382
	{
383
		if (isset($this->_memcached, $this->_lock_key) && $this->_lock)
384
		{
385
			if ( ! $this->_memcached->delete($this->_lock_key) && $this->_memcached->getResultCode() !== Memcached::RES_NOTFOUND)
386
			{
387
				log_message('error', 'Session: Error while trying to free lock for '.$this->_lock_key);
388
				return FALSE;
389
			}
390
 
391
			$this->_lock_key = NULL;
392
			$this->_lock = FALSE;
393
		}
394
 
395
		return TRUE;
396
	}
2049 lars 397
}