123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669 |
- <?php
-
- /**
- +-----------------------------------------------------------------------+
- | This file is part of the Roundcube Webmail client |
- | Copyright (C) 2005-2014, The Roundcube Dev Team |
- | Copyright (C) 2011, Kolab Systems AG |
- | |
- | Licensed under the GNU General Public License version 3 or |
- | any later version with exceptions for skins & plugins. |
- | See the README file for a full license statement. |
- | |
- | PURPOSE: |
- | Provide database supported session management |
- +-----------------------------------------------------------------------+
- | Author: Thomas Bruederli <roundcube@gmail.com> |
- | Author: Aleksander Machniak <alec@alec.pl> |
- | Author: Cor Bosman <cor@roundcu.be> |
- +-----------------------------------------------------------------------+
- */
-
- /**
- * Abstract class to provide database supported session storage
- *
- * @package Framework
- * @subpackage Core
- * @author Thomas Bruederli <roundcube@gmail.com>
- * @author Aleksander Machniak <alec@alec.pl>
- */
- abstract class rcube_session
- {
- protected $config;
- protected $key;
- protected $ip;
- protected $changed;
- protected $start;
- protected $vars;
- protected $now;
- protected $time_diff = 0;
- protected $reloaded = false;
- protected $appends = array();
- protected $unsets = array();
- protected $gc_enabled = 0;
- protected $gc_handlers = array();
- protected $cookiename = 'roundcube_sessauth';
- protected $ip_check = false;
- protected $logging = false;
-
-
- /**
- * Blocks session data from being written to database.
- * Can be used if write-race conditions are to be expected
- * @var boolean
- */
- public $nowrite = false;
-
- /**
- * Factory, returns driver-specific instance of the class
- *
- * @param object $config
- * @return Object rcube_session
- */
- public static function factory($config)
- {
- // get session storage driver
- $storage = $config->get('session_storage', 'db');
-
- // class name for this storage
- $class = "rcube_session_" . $storage;
-
- // try to instantiate class
- if (class_exists($class)) {
- return new $class($config);
- }
-
- // no storage found, raise error
- rcube::raise_error(array('code' => 604, 'type' => 'session',
- 'line' => __LINE__, 'file' => __FILE__,
- 'message' => "Failed to find session driver. Check session_storage config option"),
- true, true);
- }
-
- /**
- * @param Object $config
- */
- public function __construct($config)
- {
- $this->config = $config;
-
- // set ip check
- $this->set_ip_check($this->config->get('ip_check'));
-
- // set cookie name
- if ($this->config->get('session_auth_name')) {
- $this->set_cookiename($this->config->get('session_auth_name'));
- }
- }
-
- /**
- * register session handler
- */
- public function register_session_handler()
- {
- ini_set('session.serialize_handler', 'php');
-
- // set custom functions for PHP session management
- session_set_save_handler(
- array($this, 'open'),
- array($this, 'close'),
- array($this, 'read'),
- array($this, 'sess_write'),
- array($this, 'destroy'),
- array($this, 'gc')
- );
- }
-
- /**
- * Wrapper for session_start()
- */
- public function start()
- {
- $this->start = microtime(true);
- $this->ip = rcube_utils::remote_addr();
- $this->logging = $this->config->get('log_session', false);
-
- $lifetime = $this->config->get('session_lifetime', 1) * 60;
- $this->set_lifetime($lifetime);
-
- session_start();
- }
-
- /**
- * Abstract methods should be implemented by driver classes
- */
- abstract function open($save_path, $session_name);
- abstract function close();
- abstract function destroy($key);
- abstract function read($key);
- abstract function write($key, $vars);
- abstract function update($key, $newvars, $oldvars);
-
- /**
- * session write handler. This calls the implementation methods for write/update after some initial checks.
- *
- * @param $key
- * @param $vars
- *
- * @return bool
- */
- public function sess_write($key, $vars)
- {
- if ($this->nowrite) {
- return true;
- }
-
- // check cache
- $oldvars = $this->get_cache($key);
-
- // if there are cached vars, update store, else insert new data
- if ($oldvars) {
- $newvars = $this->_fixvars($vars, $oldvars);
- return $this->update($key, $newvars, $oldvars);
- }
- else {
- return $this->write($key, $vars);
- }
- }
-
- /**
- * Wrapper for session_write_close()
- */
- public function write_close()
- {
- session_write_close();
-
- // write_close() is called on script shutdown, see rcube::shutdown()
- // execute cleanup functionality if enabled by session gc handler
- // we do this after closing the session for better performance
- $this->gc_shutdown();
- }
-
- /**
- * Merge vars with old vars and apply unsets
- */
- protected function _fixvars($vars, $oldvars)
- {
- if ($oldvars !== null) {
- $a_oldvars = $this->unserialize($oldvars);
- if (is_array($a_oldvars)) {
- // remove unset keys on oldvars
- foreach ((array)$this->unsets as $var) {
- if (isset($a_oldvars[$var])) {
- unset($a_oldvars[$var]);
- }
- else {
- $path = explode('.', $var);
- $k = array_pop($path);
- $node = &$this->get_node($path, $a_oldvars);
- unset($node[$k]);
- }
- }
-
- $newvars = $this->serialize(array_merge(
- (array)$a_oldvars, (array)$this->unserialize($vars)));
- }
- else {
- $newvars = $vars;
- }
- }
-
- $this->unsets = array();
- return $newvars;
- }
-
- /**
- * Execute registered garbage collector routines
- */
- public function gc($maxlifetime)
- {
- // move gc execution to the script shutdown function
- // see rcube::shutdown() and rcube_session::write_close()
- $this->gc_enabled = $maxlifetime;
-
- return true;
- }
-
- /**
- * Register additional garbage collector functions
- *
- * @param mixed Callback function
- */
- public function register_gc_handler($func)
- {
- foreach ($this->gc_handlers as $handler) {
- if ($handler == $func) {
- return;
- }
- }
-
- $this->gc_handlers[] = $func;
- }
-
- /**
- * Garbage collector handler to run on script shutdown
- */
- protected function gc_shutdown()
- {
- if ($this->gc_enabled) {
- foreach ($this->gc_handlers as $fct) {
- call_user_func($fct);
- }
- }
- }
-
- /**
- * Generate and set new session id
- *
- * @param boolean $destroy If enabled the current session will be destroyed
- * @return bool
- */
- public function regenerate_id($destroy=true)
- {
- session_regenerate_id($destroy);
-
- $this->vars = null;
- $this->key = session_id();
-
- return true;
- }
-
- /**
- * See if we have vars of this key already cached, and if so, return them.
- *
- * @param string $key Session ID
- *
- * @return string
- */
- protected function get_cache($key)
- {
- // no session data in cache (read() returns false)
- if (!$this->key) {
- $cache = null;
- }
- // use internal data for fast requests (up to 0.5 sec.)
- else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
- $cache = $this->vars;
- }
- else { // else read data again
- $cache = $this->read($key);
- }
-
- return $cache;
- }
-
- /**
- * Append the given value to the certain node in the session data array
- *
- * Warning: Do not use if you already modified $_SESSION in the same request (#1490608)
- *
- * @param string Path denoting the session variable where to append the value
- * @param string Key name under which to append the new value (use null for appending to an indexed list)
- * @param mixed Value to append to the session data array
- */
- public function append($path, $key, $value)
- {
- // re-read session data from DB because it might be outdated
- if (!$this->reloaded && microtime(true) - $this->start > 0.5) {
- $this->reload();
- $this->reloaded = true;
- $this->start = microtime(true);
- }
-
- $node = &$this->get_node(explode('.', $path), $_SESSION);
-
- if ($key !== null) {
- $node[$key] = $value;
- $path .= '.' . $key;
- }
- else {
- $node[] = $value;
- }
-
- $this->appends[] = $path;
-
- // when overwriting a previously unset variable
- if ($this->unsets[$path]) {
- unset($this->unsets[$path]);
- }
- }
-
- /**
- * Unset a session variable
- *
- * @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5)
- * @return boolean True on success
- */
- public function remove($var=null)
- {
- if (empty($var)) {
- return $this->destroy(session_id());
- }
-
- $this->unsets[] = $var;
-
- if (isset($_SESSION[$var])) {
- unset($_SESSION[$var]);
- }
- else {
- $path = explode('.', $var);
- $key = array_pop($path);
- $node = &$this->get_node($path, $_SESSION);
- unset($node[$key]);
- }
-
- return true;
- }
-
- /**
- * Kill this session
- */
- public function kill()
- {
- $this->vars = null;
- $this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
- $this->destroy(session_id());
- rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
- }
-
- /**
- * Re-read session data from storage backend
- */
- public function reload()
- {
- // collect updated data from previous appends
- $merge_data = array();
- foreach ((array)$this->appends as $var) {
- $path = explode('.', $var);
- $value = $this->get_node($path, $_SESSION);
- $k = array_pop($path);
- $node = &$this->get_node($path, $merge_data);
- $node[$k] = $value;
- }
-
- if ($this->key) {
- $data = $this->read($this->key);
- }
-
- if ($data) {
- session_decode($data);
-
- // apply appends and unsets to reloaded data
- $_SESSION = array_merge_recursive($_SESSION, $merge_data);
-
- foreach ((array)$this->unsets as $var) {
- if (isset($_SESSION[$var])) {
- unset($_SESSION[$var]);
- }
- else {
- $path = explode('.', $var);
- $k = array_pop($path);
- $node = &$this->get_node($path, $_SESSION);
- unset($node[$k]);
- }
- }
- }
- }
-
- /**
- * Returns a reference to the node in data array referenced by the given path.
- * e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments']
- */
- protected function &get_node($path, &$data_arr)
- {
- $node = &$data_arr;
- if (!empty($path)) {
- foreach ((array)$path as $key) {
- if (!isset($node[$key]))
- $node[$key] = array();
- $node = &$node[$key];
- }
- }
-
- return $node;
- }
-
- /**
- * Serialize session data
- */
- protected function serialize($vars)
- {
- $data = '';
- if (is_array($vars)) {
- foreach ($vars as $var=>$value)
- $data .= $var.'|'.serialize($value);
- }
- else {
- $data = 'b:0;';
- }
-
- return $data;
- }
-
- /**
- * Unserialize session data
- * http://www.php.net/manual/en/function.session-decode.php#56106
- */
- protected function unserialize($str)
- {
- $str = (string)$str;
- $endptr = strlen($str);
- $p = 0;
-
- $serialized = '';
- $items = 0;
- $level = 0;
-
- while ($p < $endptr) {
- $q = $p;
- while ($str[$q] != '|')
- if (++$q >= $endptr)
- break 2;
-
- if ($str[$p] == '!') {
- $p++;
- $has_value = false;
- }
- else {
- $has_value = true;
- }
-
- $name = substr($str, $p, $q - $p);
- $q++;
-
- $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
-
- if ($has_value) {
- for (;;) {
- $p = $q;
- switch (strtolower($str[$q])) {
- case 'n': // null
- case 'b': // boolean
- case 'i': // integer
- case 'd': // decimal
- do $q++;
- while ( ($q < $endptr) && ($str[$q] != ';') );
- $q++;
- $serialized .= substr($str, $p, $q - $p);
- if ($level == 0)
- break 2;
- break;
- case 'r': // reference
- $q+= 2;
- for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++)
- $id .= $str[$q];
- $q++;
- // increment pointer because of outer array
- $serialized .= 'R:' . ($id + 1) . ';';
- if ($level == 0)
- break 2;
- break;
- case 's': // string
- $q+=2;
- for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++)
- $length .= $str[$q];
- $q+=2;
- $q+= (int)$length + 2;
- $serialized .= substr($str, $p, $q - $p);
- if ($level == 0)
- break 2;
- break;
- case 'a': // array
- case 'o': // object
- do $q++;
- while ($q < $endptr && $str[$q] != '{');
- $q++;
- $level++;
- $serialized .= substr($str, $p, $q - $p);
- break;
- case '}': // end of array|object
- $q++;
- $serialized .= substr($str, $p, $q - $p);
- if (--$level == 0)
- break 2;
- break;
- default:
- return false;
- }
- }
- }
- else {
- $serialized .= 'N;';
- $q += 2;
- }
- $items++;
- $p = $q;
- }
-
- return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
- }
-
- /**
- * Setter for session lifetime
- */
- public function set_lifetime($lifetime)
- {
- $this->lifetime = max(120, $lifetime);
-
- // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
- $now = time();
- $this->now = $now - ($now % ($this->lifetime / 2));
- }
-
- /**
- * Getter for remote IP saved with this session
- */
- public function get_ip()
- {
- return $this->ip;
- }
-
- /**
- * Setter for cookie encryption secret
- */
- function set_secret($secret = null)
- {
- // generate random hash and store in session
- if (!$secret) {
- if (!empty($_SESSION['auth_secret'])) {
- $secret = $_SESSION['auth_secret'];
- }
- else {
- $secret = rcube_utils::random_bytes(strlen($this->key));
- }
- }
-
- $_SESSION['auth_secret'] = $secret;
- }
-
- /**
- * Enable/disable IP check
- */
- function set_ip_check($check)
- {
- $this->ip_check = $check;
- }
-
- /**
- * Setter for the cookie name used for session cookie
- */
- function set_cookiename($cookiename)
- {
- if ($cookiename) {
- $this->cookiename = $cookiename;
- }
- }
-
- /**
- * Check session authentication cookie
- *
- * @return boolean True if valid, False if not
- */
- function check_auth()
- {
- $this->cookie = $_COOKIE[$this->cookiename];
- $result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
-
- if (!$result) {
- $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
- }
-
- if ($result && $this->_mkcookie($this->now) != $this->cookie) {
- $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
- $result = false;
-
- // Check if using id from a previous time slot
- for ($i = 1; $i <= 2; $i++) {
- $prev = $this->now - ($this->lifetime / 2) * $i;
- if ($this->_mkcookie($prev) == $this->cookie) {
- $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
- $this->set_auth_cookie();
- $result = true;
- }
- }
- }
-
- if (!$result) {
- $this->log("Session authentication failed for " . $this->key
- . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
- }
-
- return $result;
- }
-
- /**
- * Set session authentication cookie
- */
- public function set_auth_cookie()
- {
- $this->cookie = $this->_mkcookie($this->now);
- rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
- $_COOKIE[$this->cookiename] = $this->cookie;
- }
-
- /**
- * Create session cookie for specified time slot.
- *
- * @param int Time slot to use
- *
- * @return string
- */
- protected function _mkcookie($timeslot)
- {
- // make sure the secret key exists
- $this->set_secret();
-
- // no need to hash this, it's just a random string
- return $_SESSION['auth_secret'] . '-' . $timeslot;
- }
-
- /**
- * Writes debug information to the log
- */
- function log($line)
- {
- if ($this->logging) {
- rcube::write_log('session', $line);
- }
- }
- }
|