Skip to content

Loading…

Ajax Session Driver - Allow Sessions to handle simultaneous requests #1900

Closed
wants to merge 5 commits into from

10 participants

@Areson

This is in response to issue #154, and a rework of #1283 so that the changes are now bundled into a new session driver, tentatively called the "ajax" driver. For the sake of not having to link back, here is the overview of the changes, originally stated in #1283, with slight edits for clarity:

Overview

This update allows sessions to work gracefully with fast/simultaneous ajax requests without needing to simply ignore updating the session. Users are allowed to have more than one valid session id at any given time, but only one is considered to be "active." The active session id is allowed to be updated (e.g. generate a new session id, set user data, etc.), but all others session IDs associated with the user are marked as "inactive" and are read-only. Inactive sessions exist solely to make sure the requests that are created near the time a session is regenerated behave properly.

To facilitate this change an additional expiration time has been added, resulting in three "timeouts" for sessions:

  • Session Expiration: The point at which any request is destroyed
  • Time to Update: The point at which the active session id is regenerated
  • Multisession Expiration: The point which an inactive session is no longer valid. Any requests made with an inactive session id after this point are destroyed.

Here is an example of a session lifetime:

  • User interacts with the site for the first time, creating a new and active session, Session 1
  • After the Time to Update has passed, Session 1 is "updated." This process results in a new id being generated and associated with the new active session, Session 2. Session 1 is marked as inactive.
  • An AJAX request comes in with Session 1. Since it is marked as inactive, no update occurs, but the AJAX session completes because it has a valid session.
  • The multisession expiration passes
  • At this point, one of two final outcomes can happen:
    • Another AJAX request comes in with Session 1. Since the multisession expiration has passed, the session is destroyed and the AJAX request fails due to not having a valid session. (This is possible if the multisession expiration is set too low)
    • The session expiration passes for Session 1. The session will be destroyed during the course of the normal cleanup processes for sessions. Any request coming in for that session id will fail.

Changes from #1283/Important Notes

  • This driver requires the use of a database to back the sessions. As such, the option to enable/disable the database has been removed as an option for this driver. It is assumed to be available and the user only needs to provided the name of the table.
  • The multisession expiration (set by the option sess_multi_expiration) must be set sufficiently high so that any long running requests will finish before it expires. If it is set too short, dropped sessions may occur.
  • #1283 required an alteration to the session table structure. The ajax driver does not require this and will work with the current session table structure.
  • This driver does not provide any session locking outside what is needed to make sure that session regeneration happens properly. Simultaneous requests can still wipe out each other's data!
  • The driver provides a method, called multisession_expired which can be used to determine if the session associated with the request is inactive and therefore read-only
Areson added some commits
@Areson Areson Initial commit. Porting over the original work done before the sessio…
…ns were moved to the driver model.

Signed-off-by: Ian Oberst <areson@gmail.com>
b8e10ee
@Areson Areson By taking advantage of some of the changes present in the Session_coo…
…kie driver I eliminated the need to have a extra column in the session table for storing if the multisession expired. The ajax driver can no work as a drop-in driver with no database changes needed.

Signed-off-by: Ian Oberst <areson@gmail.com>
f1b6eb2
@Areson Areson Initial commit. Porting over the original work done before the sessio…
…ns were moved to the driver model.

Signed-off-by: Ian Oberst <areson@gmail.com>
c3560e9
@Areson Areson By taking advantage of some of the changes present in the Session_coo…
…kie driver I eliminated the need to have a extra column in the session table for storing if the multisession expired. The ajax driver can no work as a drop-in driver with no database changes needed.

Signed-off-by: Ian Oberst <areson@gmail.com>
7cb7cc5
@Areson Areson Merge branch 'features/AjaxSessionDriver' of https://github.com/Areso…
…n/CodeIgniter into features/AjaxSessionDriver
4191fd4
@narfbg

To be honest - I've always thought of CI's Session class to be way more complicated than it should be. This however is not complicated - it's simply wrong. Ajax is a client-side technology, you can't make a server-side "driver" for it.

@dchill42

To be fair, the name is somewhat misleading. I understand this to be a locking session driver aimed at eliminating most of the problems with concurrent or overlapping client requests (which often come via AJAX).

@GDmac

The latest session drivers don't regenerate a session_id during ajax requests.
If this still gives issues, please can you give an example?

Instead of an almost exact copy of the cookie driver, personally i'ld much rather have a fix
or mechanism that: on regenerate id, locks the session briefly and, passes the new
session_id on to next incoming requests somehow.

Race conditions are terrible to debug, or to be ready for or to prevent,
http://thwartedefforts.org/2006/11/11/race-conditions-with-ajax-and-php-sessions/

@Areson

@narfbg I agree with @dchill42... the name is misleading. I more chose the name because it was meant to resolve the issue of dropped sessions that can occur when the session is regenerated during simultaneous AJAX requests, hence the "ajax" driver. I can certainly change the name of the driver.

@GDmac The current cookie driver doesn't regenerate a session_id during AJAX requests, and this does solve the problem of dropped sessions. However, this "bypass" of session regeneration makes things a bit more difficult when designing single page web applications, which is the case that I ran into. Since the code doesn't allow the session to regenerate during AJAX requests, I'm left with a few options:

  • Don't regenerate the session for the web application
  • Have the client side code periodically "pause" all outgoing AJAX requests and request that the session be regenerated
  • Have specific events in my server side code regenerate the session, preferably at times when I know that there won't be simultaneous requests

None of these options particularly thrill me, and they all force me to do additional work to simply get the session to regenerate. At the time, I chose to not use CodeIgniter's Session library and wrote my own that performed essentially the same function that this pull request does.

As for the near copy of the cookie driver, I wasn't sure if the people would prefer to have a separate driver so that the baseline cookie driver would be left alone, or if I should just modify the existing cookie driver, which would be most similar to what I had done originally in #1283. If the general feeling is that this should exist as modifications to the cookie driver rather than its own driver, I can certainly do that.

@daparky

This is a very good addition. Even if this is 'right' it still needs to be resolved in the session drivers to allow AJAX.

@patricksavalle

"This however is not complicated - it's simply wrong. Ajax is a client-side technology, you can't make a server-side "driver" for it."

All sessions are initiated client-side. And need a server-side solution.

I admit not having read all discussion about the AJAX session problems in CI, but isn't just a matter of locking and unlocking when updating? Making the session update a transaction? Looks to me like a very standard concurrency problem.

@Areson

@patricksavalle It really is just a concurrency problem, and the current solution in CodeIgniter's session drivers is simply to not allow AJAX requests to cause the session to regenerate. As noted in my earlier response, this isn't always an acceptable solution.

However, the solution isn't as simple as just adding a lock around the session update. If your application can have multiple AJAX requests firing around the same time, then there is a potential for the session being dropped when it is update while other requests have already been sent to the server, even if you lock the session update. The situation would look something like this:

  • Request A with a session id of 123 is sent from the client
  • The session lock is initiated and the session is read on the server side
  • While the session is being read, Request B is sent from the client, also with a session id of 123
  • The session is finished being read for Request A is it is time to update the session.
    • The session is now 456 and 123 is no longer valid. A cookie is sent back to the client with the new session id of 456.
    • The session lock is released.
  • The session for Request B is locked and read in.
    • The session id associated with it, 123, is no longer valid.
    • The session driver generates a new session id of 789 and sends back a new cookie to the client. This cookie overwrites the 456 cookie, which means we no longer have a cookie for our actual session.
    • The session lock is released.
  • At this point the user would be forcibly logged since they no longer have a valid session.

Contrary to what the name of the pull request may lead people to believe (it was probably a poor name choice on my part) this is a server-side solution. It wraps the session regeneration in a lock, but also keeps the old session id around for a user specified amount of time. This allows any requests that were in flight at the time of the session update to be considered "valid" session and not drop the user. However, these "old" session ids are read-only, cannot cause the session id to be regenerated, and have a flag so that the programmer can take additional action if desired when this situation comes up. Once the user specified window for allowing these "old" session ids has passed, they are treated as expired sessions and will drop the user if they send a request with one. If the user specified window is set appropriately this will likely not happen at all.

@GDmac

This is not solely an ajax problem. I tested this with the following regular controler method. Whenever request A regenerates the session_id, then the waiting request B will operate on the old session_id. in the example i forcibly regenerate the id to show the issue.

Load this in a controller in two browser tabs, and load both within a sec. or two (sleep).

public function index()
{
    $this->load->library('session');
    $foo = $this->session->native->userdata('foo');
    $foo++;
    echo "<pre>{$foo}</pre>";
    $this->session->native->set_userdata('foo', $foo);
    $this->session->sess_regenerate();
    sleep(3);
}

@Areson instead of having multiple "valid" sessions id's, isn't there an elegant way to pass the new session_id (or actually, the whole dataset) to the waiting processes?

@GDmac

@areson in your pull request you're using a native php session for locking, also i see it does a ini_set(session.use_cookies) without cleanup afterwards (what about using multiple drivers together? e.g. the native driver does need to set a cookie!). When using php native session already, why not work on the "native" driver then?

My proposal is to leave the original session_cookie driver for what it is for the time being,
(and keep it 100% backwards compatible), and do these changes on the new native session driver.

We could work on the php native driver to add database support in (via session_save_handler),
and add a more fine-grained locking control to it (database and/or file based).
(see linked article a few comments back),
and add a config option for session_save_path (for shared hosts) etc.

@Areson

@GDmac In answer to your first question, I think there is, though I chose not to take that route because I was concerned about performance, as the solution I thought up requires at least one additional query to the database and/or a structure change. In essence, you could update the record for the old session id to include the ID of the new session. When reading in an old session, you could use that information to pull the data and ID from the new session. Unless you modify the structure of the database, this would likely require a second query when reading in an old session.

As to your second question, I don't believe my solution as it stands now would work using the native session handler. In order to prevent dropped sessions you have to control cookies fairly tightly in order to prevent a cookie with an older session id from being written. As I understand it, session_start sends a cookie to the client as part of its operation, so we have to know before starting the native session whether or not we want to send a cookie. This means we cannot prevent a cookie with an old session_id from being read in and overwriting the cookie with the new session id we previously sent.

Of course, you can always write out a new cookie once you realize you overwrote the new session id, but there are a couple of things that you'll need to overcome. First, we'll need database support for the native session in order somehow find the new session id. Second, overwriting the new session id on the client means that the client can send more requests with an old session id. This has the (unlikely?) potential of creating a self-sustaining problem where the cookie on the client flip-flops between the new session id and the old, and depending on which session id is in the cookie, cause dropped sessions.

The last problem is more of a security concern. Part of the reason I chose not to allow old sessions to write cookies out to the client was to avoid the issues stated above. The second is that allowing an old session id to give us back the new session id means that we've opened the door for someone to hijack a session with an old session id. This would be more or less of a concern depending on how we implemented our solution.

That's the cons. I do think we could take the route you are suggesting. Off the top of my head, it seems like we would have to pair the switch over to native sessions along with implementing the "elegant" passing of the new session id to the old session id. That, and adding DB support to native sessions, I think it would work. Something like this:

  • Read the session (cookie gets written)
  • Grab the session data from the database. If there is a newer session id, grab that id and the information
    • If the session is an older id, make sure it is within the timeframe allowed for getting the new session id. If not, kill it
    • If it is within the allowed timeframe, load the new session
  • Normal stuff

We'd have to have column in the database to store the reference to the new session id. That would also allow us read in the old and new session data in a single query when reading the session. When updating the session, we'd search for all of the rows that reference the old session id (in the "new session id" column) and point them at the new session id.

The only issue left that I haven't dealt with here is one that I mentioned earlier: the native session will write a cookie back to the client with an old session id when we initially read it. We'll overwrite it later, but that still means we open ourselves up to allowing the client to make more requests with the old session id. I think with what I outlined above that this generally wouldn't cause problems, but there is definitely the potential for dropped sessions if you have a fairly long sequence of rapidly send AJAX requests.

In all, I think I still prefer not using the native sessions, if only because I get better control over when and how cookies get sent to the client. Using something similar to the cookie session driver means we'll never have the cookie problem I mentioned above (though, it is debatable how much of a problem that would cause). But, I'm happy to go with what people feel like is going to be best for CI in general.

@dchill42

It sounds to me like this might be a feature worth including in the main driver library, with implementation details handled in individual drivers as necessary. I haven't drilled down into the mechanics yet, but I gather that it could be applied to either of the current backends - cookie and native.

Also, bear in mind that while session_start() does set a cookie, it can easily be negated if the associated session id is deemed invalid during processing. Look at the native implementation of sess_destroy() for an example. On top of that, I don't think the cookie data gets delivered to the client immediately, either. There is some flexibility in what the driver ultimately sends during the request.

@dchill42

See #1746 for some of the other discussion that has happened about session locking and its implications.

@narfbg

Closing this one, for numerous reasons:

  • Whatever feature this brings, it should be implemented in existing drivers and not as a separate one.
  • The problem that this PR aims to solve has only 1 real solution - read/write locks, anything else is a feaky hack (I've said this on another PR, not sure which one exactly).
  • This is actually more than a hack - it's labeled as a feature and yet it uses to its advantage a flaw that any security expert would say that is a vulnerability.

Nonetheless, thanks @Areson for the effort - it's not unnoticed, but it's not in the right direction - sorry.

@narfbg narfbg closed this
@Areson

@narfbg No worries! @dchill42 has been reworking session locking and forwarding in #1940, so hopefully a better solution that covers what I was going for will come out of that. Out of curiosity, what is the security flaw you mentioned?

@samson-htw

The other freaky hack referenced might be #1713. I think the security flaw has to do with allowing a stale session id to access the current session, which potentially opens up the session to a third party using the stale session id.

@Areson

@samson-htw That's the route that my mind went, though I wasn't sure if @narfbg had something different in mind.

@narfbg

Yep - you're enabling a race condition. If that was acceptable, we could just not regenerate the session id. :)

@8snf

Tip for those that still experience the problem like me.

http://blog.tiger-workshop.com/firephp-firefox-extension-causing-codeigniter-session-lost/

Firebug+FirePHP change the user agent when firebug is enabled/disabled and this causes session to die with little to no traces redirecting with header 302 to index.

This issue may be causing headache to others too.

@ajwhite

Yes - this is an incredible headache. Is there still no solution for this?

@Areson
@ajwhite

@Areson Do you see any fallbacks with checking if the request is an ajax request and skipping $this->sess_update()?

@Areson
@ajwhite

@Areson I hear you on that, dynamically loaded single-page sites would defeat the purpose of this check, as we'd never reach a real session update.. Luckily, my situation just requires a lot of asynchronous loading of data into reporting tables and graphs that would normally kill load time.. So successive requests during this post page asynchronous loading of data have screwed me a few times during session refresh intervals.. I'll go ahead and add this little hack in there to ingore ajax requests. Thanks for the brief discussion.

@vancouverwill

has anyone come up with any good tests to verify this does or doesn't happen. Currently I get the logging out issue super sporadically.
I have tried using brute force but that doesn't necessarily bring out the problem either. It would be good to prove it is happening so I can at least test on that.
thanks

@GDmac

test is: regenerating the session_id while other requests are still waiting or in the queue, these other requests use the old session_id, see test code in comment above 1900#issuecomment-9630963

(edit: either when session regenerate timeout kicks in, or when forcibly regenerating, as in the example code above).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 17, 2012
  1. @Areson

    Initial commit. Porting over the original work done before the sessio…

    Areson committed
    …ns were moved to the driver model.
    
    Signed-off-by: Ian Oberst <areson@gmail.com>
  2. @Areson

    By taking advantage of some of the changes present in the Session_coo…

    Areson committed
    …kie driver I eliminated the need to have a extra column in the session table for storing if the multisession expired. The ajax driver can no work as a drop-in driver with no database changes needed.
    
    Signed-off-by: Ian Oberst <areson@gmail.com>
  3. @Areson

    Initial commit. Porting over the original work done before the sessio…

    Areson committed
    …ns were moved to the driver model.
    
    Signed-off-by: Ian Oberst <areson@gmail.com>
  4. @Areson

    By taking advantage of some of the changes present in the Session_coo…

    Areson committed
    …kie driver I eliminated the need to have a extra column in the session table for storing if the multisession expired. The ajax driver can no work as a drop-in driver with no database changes needed.
    
    Signed-off-by: Ian Oberst <areson@gmail.com>
  5. @Areson
Showing with 977 additions and 1 deletion.
  1. +2 −1 system/libraries/Session/Session.php
  2. +975 −0 system/libraries/Session/drivers/Session_ajax.php
View
3 system/libraries/Session/Session.php
@@ -86,7 +86,8 @@ public function __construct(array $params = array())
// Get valid drivers list
$this->valid_drivers = array(
'Session_native',
- 'Session_cookie'
+ 'Session_cookie',
+ 'Session_ajax'
);
$key = 'sess_valid_drivers';
$drivers = isset($params[$key]) ? $params[$key] : $CI->config->item($key);
View
975 system/libraries/Session/drivers/Session_ajax.php
@@ -0,0 +1,975 @@
+<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
+/**
+ * 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 - 2012, 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
+ */
+
+/**
+ * Ajax compatible cookie-based session management driver
+ *
+ * This is a driver that allows for CodeIgniter to provide cookie-based
+ * session management that allows sessions to be regenerated during
+ * ajax requests
+ *
+ * @package CodeIgniter
+ * @subpackage Libraries
+ * @category Sessions
+ * @author Areson
+ * @link http://codeigniter.com/user_guide/libraries/sessions.html
+ */
+class CI_Session_ajax 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;
+
+ /**
+ * Length of time (in seconds) for multisessions to expire
+ *
+ * @var int
+ */
+ public $sess_multi_expiration = 15;
+
+ /**
+ * 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;
+
+ /**
+ * 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;
+
+ /**
+ * Indicates that a session can no longer update itself, as it
+ * has expired and has become read-only multisession
+ *
+ * @var bool
+ */
+ protected $prevent_update = FALSE;
+
+ const PREVENT_UPDATE_KEY = ':ajax_session:prevent_update';
+
+ /**
+ * Initialize 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_use_database',
+ 'sess_table_name',
+ 'sess_expiration',
+ 'sess_multi_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'
+ );
+
+ foreach ($prefs as $key)
+ {
+ $this->$key = isset($this->_parent->params[$key])
+ ? $this->_parent->params[$key]
+ : $this->CI->config->item($key);
+ }
+
+ if ($this->encryption_key === '')
+ {
+ show_error('In order to use the Ajax Session driver you are required to set an encryption key in your config file.');
+ }
+
+ // Load the string helper so we can use the strip_slashes() function
+ $this->CI->load->helper('string');
+
+ // Do we need encryption? If so, load the encryption class
+ if ($this->sess_encrypt_cookie === TRUE)
+ {
+ $this->CI->load->library('encrypt');
+ }
+
+ // Check for database
+ if ($this->sess_table_name === '')
+ {
+ show_error('In order to use the Ajax Session driver you are required to set the name of the session database table.');
+ }
+
+ // 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
+ $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()
+ {
+ // Only allow the session to update if it has not expired
+ // and is still allowed to update
+ if (!$this->prevent_update)
+ {
+ $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
+ $this->_multisess_destroy();
+
+ // Kill the cookie
+ $this->_setcookie($this->sess_cookie_name, addslashes(serialize(array())), ($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()
+ {
+ // Fetch the cookie
+ $session = $this->CI->input->cookie($this->sess_cookie_name);
+
+ // No cookie? Goodbye cruel world!...
+ if ($session === NULL)
+ {
+ log_message('error', 'A session cookie was not found.');
+ return FALSE;
+ }
+
+ // Check for encryption
+ if ($this->sess_encrypt_cookie === TRUE)
+ {
+ // Decrypt the cookie data
+ $session = $this->CI->encrypt->decode($session);
+ }
+ else
+ {
+ // Encryption was not used, so we need to check the md5 hash in the last 32 chars
+ $len = strlen($session)-32;
+ $hash = substr($session, $len);
+ $session = substr($session, 0, $len);
+
+ // Does the md5 hash match? This is to prevent manipulation of session data in userspace
+ if ($hash !== md5($session.$this->encryption_key))
+ {
+ log_message('error', 'The session cookie data did not match what was expected. This could be a possible hacking attempt.');
+ $this->sess_destroy();
+ return FALSE;
+ }
+ }
+
+ // Unserialize the session array
+ $session = $this->_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']))
+ {
+ $this->sess_destroy();
+ return FALSE;
+ }
+
+ // Is the session current?
+ if (($session['last_activity'] + $this->sess_expiration) < $this->now)
+ {
+ $this->sess_destroy();
+ return FALSE;
+ }
+
+ // Does the IP match?
+ if ($this->sess_match_ip === TRUE && $session['ip_address'] !== $this->CI->input->ip_address())
+ {
+ $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)))
+ {
+ $this->sess_destroy();
+ return FALSE;
+ }
+
+ //Grab a lock on this session to perform mulisession logic
+ $this->_get_multi_session($session['session_id']);
+
+ // Fetch the session data from the database
+ $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 ($query->num_rows() === 0)
+ {
+ $this->sess_destroy();
+ session_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 = $this->_unserialize($row->user_data);
+
+ if (is_array($custom_data))
+ {
+ $session = $session + $custom_data;
+ }
+ }
+
+ //Is the current session still allowed to be updated?
+ if(isset($session[self::PREVENT_UPDATE_KEY]))
+ {
+ $this->prevent_update = ($session[self::PREVENT_UPDATE_KEY] === 0)?FALSE:TRUE;
+ unset($session[self::PREVENT_UPDATE_KEY]);
+ }
+ else
+ {
+ $this->prevent_update = NULL;
+ }
+
+ // Check to see if this session doesn't exist (previously destroyed)
+ // If so, kill it.
+ if (is_null($this->prevent_update))
+ {
+ $this->sess_destroy();
+
+ // Destroy the php session
+ session_destroy();
+ return FALSE;
+ }
+
+ // Check to see if the session is an expired multisession. If it is, destroy it
+ $last_activity = $row->last_activity;
+
+ if($this->prevent_update && ($last_activity + $this->sess_multi_expiration) < $this->now)
+ {
+ $this->_multisess_destroy();
+
+ // Destroy the php session
+ session_destroy();
+ return FALSE;
+ }
+
+ // 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' => substr($this->CI->input->user_agent(), 0, 120),
+ 'last_activity' => $this->now
+ );
+
+ // Add empty user_data field and save the data to the DB
+ $userdata = array(self::PREVENT_UPDATE_KEY => 0);
+ $this->CI->db->set('user_data', $this->_serialize($userdata))->insert($this->sess_table_name, $this->userdata);
+
+ // Setup the session to store information on whether or not
+ // the session can be updated
+ $this->_get_multi_session($this->userdata['session_id']);
+ $this->prevent_update = FALSE;
+
+ session_write_close();
+
+ // 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)
+ {
+ session_write_close();
+ return;
+ }
+
+ // Check if this session is no longer allowed to update and has exired.
+ // If so, flag it as expired so we can take action as appropriate.
+ if ($this->prevent_update)
+ {
+ session_write_close();
+ return;
+ }
+
+ // Update last activity to now
+ $this->userdata['last_activity'] = $this->now;
+
+ // Save the old session id so we know which DB record to update
+ $old_sessid = $this->userdata['session_id'];
+
+ //Set the session as no longer allowing updates
+ $this->prevent_update = TRUE;
+ $this->userdata[self::PREVENT_UPDATE_KEY] = 1;
+
+ // 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?
+ $old_userdata = $this->_serialize($userdata);
+
+ // Update the last_activity and prevent_update fields in the DB
+ $this->CI->db->update($this->sess_table_name, array(
+ 'last_activity' => $this->now,
+ 'user_data' => $old_userdata
+ ), array('session_id' => $old_sessid));
+
+ //Release the session lock so other requests can process
+ session_write_close();
+
+ // Get new id
+ $this->userdata['session_id'] = $this->_make_sess_id();
+
+ // Create a new entry for the updated session id. This will be the only
+ // session id that can continue to update.
+ $this->_get_multi_session($this->userdata['session_id']);
+
+ // Clear the prevent update flag
+ $this->prevent_update = FALSE;
+ $this->userdata[self::PREVENT_UPDATE_KEY] = 0;
+
+ // 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->now,
+ 'session_id' => $this->userdata['session_id'],
+ 'ip_address' => $this->userdata['ip_address'],
+ 'user_agent' => $this->userdata['user_agent'],
+ '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'] = $this->_serialize($userdata);
+ }
+
+ // Write the new session id to the database
+ $this->CI->db->insert($this->sess_table_name, $set);
+
+ // Unset the internal session data
+ unset($this->userdata[self::PREVENT_UPDATE_KEY]);
+
+ // Release the session lock for the new session
+ session_write_close();
+
+ // 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 database and dirty flag and unsaved, and allow update only
+ // if the multisession is allowed to and has not expired
+ if ( !$this->prevent_update && $this->data_dirty === TRUE)
+ {
+ // 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);
+
+ // Make sure the internal session data is stored
+ $userdata[self::PREVENT_UPDATE_KEY] = ($this->prevent_update ? 1 : 0);
+
+ // Did we find any custom data?
+ if ( ! empty($userdata))
+ {
+ // Serialize the custom data array so we can store it
+ $set['user_data'] = $this->_serialize($userdata);
+ }
+
+ // 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->update($this->sess_table_name, $set, array('session_id' => $this->userdata['session_id']));
+
+ // Clear dirty flag to prevent double updates
+ $this->data_dirty = FALSE;
+
+ log_message('debug', 'CI_Session Data Saved To DB');
+ }
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Indicates if the session is an expired multi-session
+ *
+ * @return boolean
+ */
+ public function multisession_expired()
+ {
+ return ($this->prevent_update);
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Generate a new session id
+ *
+ * @return string Hashed session id
+ */
+ protected function _make_sess_id()
+ {
+ $new_sessid = '';
+ do
+ {
+ $new_sessid .= mt_rand(0, mt_getrandmax());
+ }
+ 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()
+ {
+ // Get userdata (only defaults if database)
+ $cookie_data = array_intersect_key($this->userdata, $this->defaults);
+
+ // Serialize the userdata for the cookie
+ $cookie_data = $this->_serialize($cookie_data);
+
+ $cookie_data = ($this->sess_encrypt_cookie === TRUE)
+ ? $this->CI->encrypt->encode($cookie_data)
+ // if encryption is not used, we provide an md5 hash to prevent userside tampering
+ : $cookie_data.md5($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);
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Serialize an array
+ *
+ * This function first converts any slashes found in the array to a temporary
+ * marker, so when it gets unserialized the slashes will be preserved
+ *
+ * @param mixed Data to serialize
+ * @return string Serialized data
+ */
+ protected function _serialize($data)
+ {
+ if (is_array($data))
+ {
+ array_walk_recursive($data, array(&$this, '_escape_slashes'));
+ }
+ elseif (is_string($data))
+ {
+ $data = str_replace('\\', '{{slash}}', $data);
+ }
+
+ return serialize($data);
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Escape slashes
+ *
+ * This function converts any slashes found into a temporary marker
+ *
+ * @param string Value
+ * @param string Key
+ * @return void
+ */
+ protected function _escape_slashes(&$val, $key)
+ {
+ if (is_string($val))
+ {
+ $val = str_replace('\\', '{{slash}}', $val);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Unserialize
+ *
+ * This function unserializes a data string, then converts any
+ * temporary slash markers back to actual slashes
+ *
+ * @param mixed Data to unserialize
+ * @return mixed Unserialized data
+ */
+ protected function _unserialize($data)
+ {
+ $data = @unserialize(strip_slashes(trim($data)));
+
+ if (is_array($data))
+ {
+ array_walk_recursive($data, array(&$this, '_unescape_slashes'));
+ return $data;
+ }
+
+ return is_string($data) ? str_replace('{{slash}}', '\\', $data) : $data;
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Unescape slashes
+ *
+ * This function converts any slash markers back into actual slashes
+ *
+ * @param string Value
+ * @param string Key
+ * @return void
+ */
+ protected function _unescape_slashes(&$val, $key)
+ {
+ if (is_string($val))
+ {
+ $val= str_replace('{{slash}}', '\\', $val);
+ }
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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');
+
+ srand(time());
+ if ((mt_rand(0, $divisor) / $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.');
+ }
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Multi-Sessions Setup
+ *
+ * Sets up a php session to handle a flag which
+ * indicates if a session id can update itself
+ * or not.
+ *
+ * @param string
+ * @return void
+ */
+ protected function _get_multi_session($session_id)
+ {
+ /* This is a bit of a hack, but we need to pass around info on
+ * if the current session can be updated or not. Starting a php
+ * session will effectively block all subsequent requests for the
+ * same session id so that we can prevent race conditions that might
+ * allow erroneous updates to the session id.
+ */
+
+ //Don't allow cookies for the php session
+ ini_set('session.use_cookies', '0');
+ ini_set('session.use_only_cookies', '0');
+
+ //Start a session using our internally generated session id
+ session_id($session_id);
+ session_start();
+ }
+
+ // ------------------------------------------------------------------------
+
+ /**
+ * Destroy the entry in the database for the multisession
+ *
+ * @return void
+ */
+ protected function _multisess_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;
+ $this->prevent_update = TRUE;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+}
+
+/* End of file Session_cookie.php */
+/* Location: ./system/libraries/Session/drivers/Session_cookie.php */
Something went wrong with that request. Please try again.