Skip to content

Instantly share code, notes, and snippets.

@ilikenwf
Last active August 29, 2015 14:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilikenwf/9893d5b21d06cab9bfa5 to your computer and use it in GitHub Desktop.
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
<?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
* licensing@ellislab.com 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