Last active
August 29, 2015 14:06
-
-
Save ilikenwf/9893d5b21d06cab9bfa5 to your computer and use it in GitHub Desktop.
Hybrid Cookie or DB session driver for codeigniter - allows for API or cookie based auth
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* CodeIgniter | |
* | |
* An open source application development framework for PHP 5.2.4 or newer | |
* | |
* NOTICE OF LICENSE | |
* | |
* Licensed under the Open Software License version 3.0 | |
* | |
* This source file is subject to the Open Software License (OSL 3.0) that is | |
* bundled with this package in the files license.txt / license.rst. It is | |
* also available through the world wide web at this URL: | |
* http://opensource.org/licenses/OSL-3.0 | |
* If you did not receive a copy of the license and are unable to obtain it | |
* through the world wide web, please send an email to | |
* [email protected] so we can send you a copy immediately. | |
* | |
* @package CodeIgniter | |
* @author EllisLab Dev Team | |
* @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/) | |
* @license http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) | |
* @link http://codeigniter.com | |
* @since Version 1.0 | |
* @filesource | |
*/ | |
defined('BASEPATH') OR exit('No direct script access allowed'); | |
/** | |
* Cookie or database based session management driver | |
* | |
* This is a hybridized version of the classic CI_Session functionality, as written by EllisLab, abstracted out to a driver. | |
* It allows for cookie storage as well as API based requests back and forth. | |
* | |
* @package CodeIgniter | |
* @subpackage Libraries | |
* @category Sessions | |
* @author EllisLab Dev Team | |
* @link http://codeigniter.com/user_guide/libraries/sessions.html | |
*/ | |
class CI_Session_hybrid extends CI_Session_driver { | |
/** | |
* Whether to encrypt the session cookie | |
* | |
* @var bool | |
*/ | |
public $sess_encrypt_cookie = FALSE; | |
/** | |
* Name of the database table in which to store sessions | |
* | |
* @var string | |
*/ | |
public $sess_table_name = ''; | |
/** | |
* Length of time (in seconds) for sessions to expire | |
* | |
* @var int | |
*/ | |
public $sess_expiration = 7200; | |
/** | |
* Whether to kill session on close of browser window | |
* | |
* @var bool | |
*/ | |
public $sess_expire_on_close = FALSE; | |
/** | |
* Whether to match session on ip address | |
* | |
* @var bool | |
*/ | |
public $sess_match_ip = FALSE; | |
/** | |
* Whether to match session on user-agent | |
* | |
* @var bool | |
*/ | |
public $sess_match_useragent = TRUE; | |
/** | |
* Name of session cookie | |
* | |
* @var string | |
*/ | |
public $sess_cookie_name = 'ci_session'; | |
/** | |
* Session cookie prefix | |
* | |
* @var string | |
*/ | |
public $cookie_prefix = ''; | |
/** | |
* Session cookie path | |
* | |
* @var string | |
*/ | |
public $cookie_path = ''; | |
/** | |
* Session cookie domain | |
* | |
* @var string | |
*/ | |
public $cookie_domain = ''; | |
/** | |
* Whether to set the cookie only on HTTPS connections | |
* | |
* @var bool | |
*/ | |
public $cookie_secure = FALSE; | |
/** | |
* Whether cookie should be allowed only to be sent by the server | |
* | |
* @var bool | |
*/ | |
public $cookie_httponly = FALSE; | |
/** | |
* Interval at which to update session | |
* | |
* @var int | |
*/ | |
public $sess_time_to_update = 300; | |
/** | |
* Key with which to encrypt the session cookie | |
* | |
* @var string | |
*/ | |
public $encryption_key = ''; | |
/** | |
* Timezone to use for the current time | |
* | |
* @var string | |
*/ | |
public $time_reference = 'local'; | |
/** | |
* Session data | |
* | |
* @var array | |
*/ | |
public $userdata = array(); | |
/** | |
* Current time | |
* | |
* @var int | |
*/ | |
public $now; | |
//key for looking up a session, may be passed in lieu of a cookie | |
public $sesslookup = NULL; | |
private $sessionFromDB = false; | |
private $usingAPI = false; | |
private $remoteIP = ''; | |
// ------------------------------------------------------------------------ | |
/** | |
* Default userdata keys | |
* | |
* @var array | |
*/ | |
protected $defaults = array( | |
'session_id' => NULL, | |
'ip_address' => NULL, | |
'user_agent' => NULL, | |
'last_activity' => NULL | |
); | |
/** | |
* Data needs DB update flag | |
* | |
* @var bool | |
*/ | |
protected $data_dirty = FALSE; | |
/** | |
* Standardize newlines flag | |
* | |
* @var bool | |
*/ | |
protected $_standardize_newlines; | |
// ------------------------------------------------------------------------ | |
/** | |
* Ini\tialize session driver object | |
* | |
* @return void | |
*/ | |
protected function initialize() | |
{ | |
// Set all the session preferences, which can either be set | |
// manually via the $params array or via the config file | |
$prefs = array( | |
'sess_encrypt_cookie', | |
'sess_table_name', | |
'sess_expiration', | |
'sess_expire_on_close', | |
'sess_match_ip', | |
'sess_match_useragent', | |
'sess_cookie_name', | |
'cookie_path', | |
'cookie_domain', | |
'cookie_secure', | |
'cookie_httponly', | |
'sess_time_to_update', | |
'time_reference', | |
'cookie_prefix', | |
'encryption_key' | |
); | |
$this->_standardize_newlines = (bool) config_item('standardize_newlines'); | |
foreach ($prefs as $key) | |
{ | |
$this->$key = isset($this->_parent->params[$key]) | |
? $this->_parent->params[$key] | |
: $this->CI->config->item($key); | |
} | |
//if we're using an API and the session is already named | |
if (!empty($this->_parent->params['usingAPI']) && $this->_parent->params['usingAPI']) { | |
$this->usingAPI = $this->_parent->params['usingAPI']; | |
if (!empty($this->_parent->params['session_id'])) { | |
$this->sesslookup = $this->_parent->params['session_id']; | |
} | |
if (!empty($this->_parent->params['remoteIP'])) { | |
$this->remoteIP = $this->_parent->params['remoteIP']; | |
} | |
} | |
if (empty($this->encryption_key)) | |
{ | |
show_error('In order to use the Cookie Session driver you are required to set an encryption key in your config file.'); | |
} | |
// Do we need encryption? If so, load the encryption class | |
if ($this->sess_encrypt_cookie === TRUE) | |
{ | |
$this->CI->load->library('encryption'); | |
} | |
// Load database driver | |
$this->CI->load->database(); | |
// Register shutdown function | |
register_shutdown_function(array($this, '_update_db')); | |
// Set the "now" time. Can either be GMT or server time, based on the config prefs. | |
// We use this to set the "last activity" time | |
$this->now = $this->_get_time(); | |
// Set the session length. If the session expiration is | |
// set to zero we'll set the expiration two years from now. | |
if ($this->sess_expiration === 0) | |
{ | |
$this->sess_expiration = (60*60*24*365*2); | |
} | |
// Set the cookie name | |
if (!empty($this->sesslookup) && $this->usingAPI) { | |
$this->sess_cookie_name = $this->cookie_prefix.$this->sesslookup; | |
} else { | |
$this->sess_cookie_name = $this->cookie_prefix.$this->sess_cookie_name; | |
} | |
// Run the Session routine. If a session doesn't exist we'll | |
// create a new one. If it does, we'll update it. | |
if ( ! $this->_sess_read()) | |
{ | |
$this->_sess_create(); | |
} | |
else | |
{ | |
$this->_sess_update(); | |
} | |
// Delete expired sessions if necessary | |
$this->_sess_gc(); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Write the session data | |
* | |
* @return void | |
*/ | |
public function sess_save() | |
{ | |
// Mark custom data as dirty so we know to update the DB | |
$this->data_dirty = TRUE; | |
// Write the cookie | |
$this->_set_cookie(); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Destroy the current session | |
* | |
* @return void | |
*/ | |
public function sess_destroy() | |
{ | |
// Kill the session DB row | |
if (isset($this->userdata['session_id'])) | |
{ | |
$this->CI->db->delete($this->sess_table_name, array('session_id' => $this->userdata['session_id'])); | |
$this->data_dirty = FALSE; | |
} | |
// Kill the cookie | |
$this->_setcookie($this->sess_cookie_name, '', ($this->now - 31500000), | |
$this->cookie_path, $this->cookie_domain, 0); | |
// Kill session data | |
$this->userdata = array(); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Regenerate the current session | |
* | |
* Regenerate the session id | |
* | |
* @param bool Destroy session data flag (default: false) | |
* @return void | |
*/ | |
public function sess_regenerate($destroy = FALSE) | |
{ | |
// Check destroy flag | |
if ($destroy) | |
{ | |
// Destroy old session and create new one | |
$this->sess_destroy(); | |
$this->_sess_create(); | |
} | |
else | |
{ | |
// Just force an update to recreate the id | |
$this->_sess_update(TRUE); | |
} | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Get a reference to user data array | |
* | |
* @return array Reference to userdata | |
*/ | |
public function &get_userdata() | |
{ | |
return $this->userdata; | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Fetch the current session data if it exists | |
* | |
* @return bool | |
*/ | |
protected function _sess_read() | |
{ | |
//API only - is the session in the db? API ignores cookies | |
if ($this->usingAPI && !empty($this->sesslookup)) | |
{ | |
$this->CI->db->where('session_id', $this->sesslookup); | |
// Is caching in effect? Turn it off | |
$db_cache = $this->CI->db->cache_on; | |
$this->CI->db->cache_off(); | |
$query = $this->CI->db->limit(1)->get($this->sess_table_name); | |
// Was caching in effect? | |
if ($db_cache) | |
{ | |
// Turn it back on | |
$this->CI->db->cache_on(); | |
} | |
$session = (array) $query->row(); | |
if (!empty($session['user_data'])) | |
{ | |
$custom_data = unserialize(trim($session['user_data'])); | |
if (is_array($custom_data)) | |
{ | |
$session = $session + $custom_data; | |
} | |
} | |
unset($session['user_data']); | |
unset($session['id']); | |
if (!empty($session)) { | |
$this->userdata = $session; | |
$this->sessionFromDB = TRUE; | |
$this->_sess_update(TRUE); | |
} else { | |
log_message('debug', 'A db session was not found.'); | |
return FALSE; | |
} | |
} else { | |
// Fetch the cookie | |
$session = $this->CI->input->cookie($this->sess_cookie_name); | |
} | |
// No session? Goodbye cruel world!... | |
if ($session === NULL) | |
{ | |
log_message('debug', 'A session cookie was not found.'); | |
return FALSE; | |
} | |
if (!$this->sessionFromDB) { | |
if ($this->sess_encrypt_cookie === TRUE) | |
{ | |
$session = $this->CI->encryption->decrypt($session); | |
if ($session === FALSE) | |
{ | |
log_message('error', 'Session: Unable to decrypt the session cookie, possibly due to a HMAC mismatch.'); | |
return FALSE; | |
} | |
} | |
else | |
{ | |
if ( ! $this->sesslookup && ($len = strlen($session) - 40) <= 0) | |
{ | |
log_message('error', 'Session: The session cookie was not signed.'); | |
return FALSE; | |
} | |
// Check cookie authentication | |
$hmac = substr($session, $len); | |
$session = substr($session, 0, $len); | |
// Time-attack-safe comparison | |
$hmac_check = hash_hmac('sha1', $session, $this->encryption_key); | |
$diff = 0; | |
for ($i = 0; $i < 40; $i++) | |
{ | |
$diff |= ord($hmac[$i]) ^ ord($hmac_check[$i]); | |
} | |
if ($diff !== 0) | |
{ | |
log_message('error', 'Session: HMAC mismatch. The session cookie data did not match what was expected.'); | |
$this->sess_destroy(); | |
return FALSE; | |
} | |
} | |
// Unserialize the session array | |
$session = @unserialize($session); | |
} | |
// Is the session data we unserialized an array with the correct format? | |
if ( ! is_array($session) OR ! isset($session['session_id'], $session['ip_address'], $session['user_agent'], $session['last_activity'])) | |
{ | |
log_message('debug', 'Session: Wrong cookie data format'); | |
$this->sess_destroy(); | |
return FALSE; | |
} | |
// Is the session current? | |
if (($session['last_activity'] + $this->sess_expiration) < $this->now OR $session['last_activity'] > $this->now) | |
{ | |
log_message('debug', 'Session: Expired'); | |
$this->sess_destroy(); | |
return FALSE; | |
} | |
// Does the IP match? | |
if ($this->sess_match_ip === TRUE && $session['ip_address'] !== $this->CI->input->ip_address()) | |
{ | |
log_message('debug', 'Session: IP address mismatch'); | |
$this->sess_destroy(); | |
return FALSE; | |
} | |
// Does the User Agent Match? | |
if ($this->sess_match_useragent === TRUE && | |
trim($session['user_agent']) !== trim(substr($this->CI->input->user_agent(), 0, 120))) | |
{ | |
log_message('debug', 'Session: User Agent string mismatch'); | |
$this->sess_destroy(); | |
return FALSE; | |
} | |
// Is there a corresponding session in the DB? | |
if (!$this->sessionFromDB) //if we queried it already, no reason to do so again | |
{ | |
$this->CI->db->where('session_id', $session['session_id']); | |
if ($this->sess_match_ip === TRUE) | |
{ | |
$this->CI->db->where('ip_address', $session['ip_address']); | |
} | |
if ($this->sess_match_useragent === TRUE) | |
{ | |
$this->CI->db->where('user_agent', $session['user_agent']); | |
} | |
// Is caching in effect? Turn it off | |
$db_cache = $this->CI->db->cache_on; | |
$this->CI->db->cache_off(); | |
$query = $this->CI->db->limit(1)->get($this->sess_table_name); | |
// Was caching in effect? | |
if ($db_cache) | |
{ | |
// Turn it back on | |
$this->CI->db->cache_on(); | |
} | |
// No result? Kill it! | |
if (empty($query) OR $query->num_rows() === 0) | |
{ | |
log_message('debug', 'Session: No match found in our database'); | |
$this->sess_destroy(); | |
return FALSE; | |
} | |
// Is there custom data? If so, add it to the main session array | |
$row = $query->row(); | |
if ( ! empty($row->user_data)) | |
{ | |
$custom_data = unserialize(trim($row->user_data)); | |
if (is_array($custom_data)) | |
{ | |
$session = $session + $custom_data; | |
} | |
} | |
} | |
// Session is valid! | |
$this->userdata = $session; | |
return TRUE; | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Create a new session | |
* | |
* @return void | |
*/ | |
protected function _sess_create() | |
{ | |
// Initialize userdata | |
$this->userdata = array( | |
'session_id' => $this->_make_sess_id(), | |
'ip_address' => $this->CI->input->ip_address(), | |
'user_agent' => trim(substr($this->CI->input->user_agent(), 0, 120)), | |
'last_activity' => $this->now, | |
); | |
if ($this->usingAPI) $this->userdata['ip_address'] = $this->remoteIP; | |
log_message('debug', 'Session: Creating new session ('.$this->userdata['session_id'].')'); | |
// Add empty user_data field and save the data to the DB | |
$this->CI->db->set('user_data', '')->insert($this->sess_table_name, $this->userdata); | |
// Write the cookie | |
$this->_set_cookie(); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Update an existing session | |
* | |
* @param bool Force update flag (default: false) | |
* @return void | |
*/ | |
protected function _sess_update($force = FALSE) | |
{ | |
// We only update the session every five minutes by default (unless forced) | |
if (!$force || ($this->userdata['last_activity'] + $this->sess_time_to_update) >= $this->now) | |
{ | |
return; | |
} | |
// Update last activity to now | |
$this->userdata['last_activity'] = $this->now; | |
// update the ip if API called | |
if ($this->sessionFromDB) $this->userdata['ip_address'] = $this->usingAPI; | |
// Save the old session id so we know which DB record to update | |
$old_sessid = $this->userdata['session_id']; | |
// Changing the session ID during an AJAX or API call causes problems | |
if (!$this->CI->input->is_ajax_request() && !$this->sessionFromDB) | |
{ | |
// Get new id | |
$this->userdata['session_id'] = $this->_make_sess_id(); | |
log_message('debug', 'Session: Regenerate ID'); | |
} | |
$this->CI->db->where('session_id', $old_sessid); | |
if ($this->sess_match_ip === TRUE) | |
{ | |
$this->CI->db->where('ip_address', $this->CI->input->ip_address()); | |
} | |
if ($this->sess_match_useragent === TRUE) | |
{ | |
$this->CI->db->where('user_agent', trim(substr($this->CI->input->user_agent(), 0, 120))); | |
} | |
// Update the session ID and last_activity field in the DB | |
$this->CI->db->update($this->sess_table_name, | |
array( | |
'last_activity' => $this->now, | |
'session_id' => $this->userdata['session_id'] | |
) | |
); | |
// Write the cookie | |
$this->_set_cookie(); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Update database with current data | |
* | |
* This gets called from the shutdown function and also | |
* registered with PHP to run at the end of the request | |
* so it's guaranteed to update even when a fatal error | |
* occurs. The first call makes the update and clears the | |
* dirty flag so it won't happen twice. | |
* | |
* @return void | |
*/ | |
public function _update_db() | |
{ | |
// Check for dirty flag and unsaved | |
if ($this->data_dirty === TRUE && !empty($this->userdata['session_id'])) | |
{ | |
// Set up activity and data fields to be set | |
// If we don't find custom data, user_data will remain an empty string | |
$set = array( | |
'last_activity' => $this->userdata['last_activity'], | |
'user_data' => '' | |
); | |
// Get the custom userdata, leaving out the defaults | |
// (which get stored in the cookie) | |
$userdata = array_diff_key($this->userdata, $this->defaults); | |
// Did we find any custom data? | |
if ( ! empty($userdata)) | |
{ | |
// Serialize the custom data array so we can store it | |
$set['user_data'] = serialize($userdata); | |
} | |
// Reset query builder values. | |
$this->CI->db->reset_query(); | |
// Run the update query | |
// Any time we change the session id, it gets updated immediately, | |
// so our where clause below is always safe | |
$this->CI->db->where('session_id', $this->userdata['session_id']); | |
if ($this->sess_match_ip === TRUE) | |
{ | |
$this->CI->db->where('ip_address', $this->CI->input->ip_address()); | |
} | |
if ($this->sess_match_useragent === TRUE) | |
{ | |
$this->CI->db->where('user_agent', trim(substr($this->CI->input->user_agent(), 0, 120))); | |
} | |
$this->CI->db->update($this->sess_table_name, $set); | |
// Clear dirty flag to prevent double updates | |
$this->data_dirty = FALSE; | |
log_message('debug', 'CI_Session Data Saved To DB'); | |
} | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Generate a new session id | |
* | |
* @return string Hashed session id | |
*/ | |
protected function _make_sess_id() | |
{ | |
$new_sessid = ''; | |
do | |
{ | |
$new_sessid .= mt_rand(); | |
} | |
while (strlen($new_sessid) < 32); | |
// To make the session ID even more secure we'll combine it with the user's IP | |
$new_sessid .= $this->CI->input->ip_address(); | |
// Turn it into a hash and return | |
return md5(uniqid($new_sessid, TRUE)); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Get the "now" time | |
* | |
* @return int Time | |
*/ | |
protected function _get_time() | |
{ | |
if ($this->time_reference === 'local' OR $this->time_reference === date_default_timezone_get()) | |
{ | |
return time(); | |
} | |
$datetime = new DateTime('now', new DateTimeZone($this->time_reference)); | |
sscanf($datetime->format('j-n-Y G:i:s'), '%d-%d-%d %d:%d:%d', $day, $month, $year, $hour, $minute, $second); | |
return mktime($hour, $minute, $second, $month, $day, $year); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Write the session cookie | |
* | |
* @return void | |
*/ | |
protected function _set_cookie() | |
{ | |
if (!$this->sessionFromDB) { | |
// Get userdata (only defaults if database) | |
$cookie_data = ($this->sessionFromDB === TRUE) | |
? array_intersect_key($this->userdata, $this->defaults) | |
: $this->userdata; | |
// The Input class will do this and since we use HMAC verification, | |
// unless we standardize here as well, the hash won't match. | |
if ($this->_standardize_newlines) | |
{ | |
foreach (array_keys($this->userdata) as $key) | |
{ | |
$this->userdata[$key] = preg_replace('/(?:\r\n|[\r\n])/', PHP_EOL, $this->userdata[$key]); | |
} | |
} | |
// Serialize the userdata for the cookie | |
$cookie_data = serialize($cookie_data); | |
if ($this->sess_encrypt_cookie === TRUE) | |
{ | |
$cookie_data = $this->CI->encryption->encrypt($cookie_data); | |
} | |
else | |
{ | |
// Require message authentication | |
$cookie_data .= hash_hmac('sha1', $cookie_data, $this->encryption_key); | |
} | |
$expire = ($this->sess_expire_on_close === TRUE) ? 0 : $this->sess_expiration + time(); | |
// Set the cookie | |
$this->_setcookie($this->sess_cookie_name, $cookie_data, $expire, $this->cookie_path, $this->cookie_domain, | |
$this->cookie_secure, $this->cookie_httponly); | |
} | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Set a cookie with the system | |
* | |
* This abstraction of the setcookie call allows overriding for unit testing | |
* | |
* @param string Cookie name | |
* @param string Cookie value | |
* @param int Expiration time | |
* @param string Cookie path | |
* @param string Cookie domain | |
* @param bool Secure connection flag | |
* @param bool HTTP protocol only flag | |
* @return void | |
*/ | |
protected function _setcookie($name, $value = '', $expire = 0, $path = '', $domain = '', $secure = FALSE, $httponly = FALSE) | |
{ | |
setcookie($name, $value, $expire, $path, $domain, $secure, $httponly); | |
} | |
// ------------------------------------------------------------------------ | |
/** | |
* Garbage collection | |
* | |
* This deletes expired session rows from database | |
* if the probability percentage is met | |
* | |
* @return void | |
*/ | |
protected function _sess_gc() | |
{ | |
$probability = ini_get('session.gc_probability'); | |
$divisor = ini_get('session.gc_divisor'); | |
if (mt_rand(1, $divisor) <= $probability) | |
{ | |
$expire = $this->now - $this->sess_expiration; | |
$this->CI->db->delete($this->sess_table_name, 'last_activity < '.$expire); | |
log_message('debug', 'Session garbage collection performed.'); | |
} | |
} | |
} | |
/* End of file Session_hybrid.php */ | |
/* Location: ./system/libraries/Session/drivers/Session_hybrid.php */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment