You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rcube_session.php 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2005-2014, The Roundcube Dev Team |
  6. | Copyright (C) 2011, Kolab Systems AG |
  7. | |
  8. | Licensed under the GNU General Public License version 3 or |
  9. | any later version with exceptions for skins & plugins. |
  10. | See the README file for a full license statement. |
  11. | |
  12. | PURPOSE: |
  13. | Provide database supported session management |
  14. +-----------------------------------------------------------------------+
  15. | Author: Thomas Bruederli <roundcube@gmail.com> |
  16. | Author: Aleksander Machniak <alec@alec.pl> |
  17. | Author: Cor Bosman <cor@roundcu.be> |
  18. +-----------------------------------------------------------------------+
  19. */
  20. /**
  21. * Abstract class to provide database supported session storage
  22. *
  23. * @package Framework
  24. * @subpackage Core
  25. * @author Thomas Bruederli <roundcube@gmail.com>
  26. * @author Aleksander Machniak <alec@alec.pl>
  27. */
  28. abstract class rcube_session
  29. {
  30. protected $config;
  31. protected $key;
  32. protected $ip;
  33. protected $changed;
  34. protected $start;
  35. protected $vars;
  36. protected $now;
  37. protected $time_diff = 0;
  38. protected $reloaded = false;
  39. protected $appends = array();
  40. protected $unsets = array();
  41. protected $gc_enabled = 0;
  42. protected $gc_handlers = array();
  43. protected $cookiename = 'roundcube_sessauth';
  44. protected $ip_check = false;
  45. protected $logging = false;
  46. /**
  47. * Blocks session data from being written to database.
  48. * Can be used if write-race conditions are to be expected
  49. * @var boolean
  50. */
  51. public $nowrite = false;
  52. /**
  53. * Factory, returns driver-specific instance of the class
  54. *
  55. * @param object $config
  56. * @return Object rcube_session
  57. */
  58. public static function factory($config)
  59. {
  60. // get session storage driver
  61. $storage = $config->get('session_storage', 'db');
  62. // class name for this storage
  63. $class = "rcube_session_" . $storage;
  64. // try to instantiate class
  65. if (class_exists($class)) {
  66. return new $class($config);
  67. }
  68. // no storage found, raise error
  69. rcube::raise_error(array('code' => 604, 'type' => 'session',
  70. 'line' => __LINE__, 'file' => __FILE__,
  71. 'message' => "Failed to find session driver. Check session_storage config option"),
  72. true, true);
  73. }
  74. /**
  75. * @param Object $config
  76. */
  77. public function __construct($config)
  78. {
  79. $this->config = $config;
  80. // set ip check
  81. $this->set_ip_check($this->config->get('ip_check'));
  82. // set cookie name
  83. if ($this->config->get('session_auth_name')) {
  84. $this->set_cookiename($this->config->get('session_auth_name'));
  85. }
  86. }
  87. /**
  88. * register session handler
  89. */
  90. public function register_session_handler()
  91. {
  92. ini_set('session.serialize_handler', 'php');
  93. // set custom functions for PHP session management
  94. session_set_save_handler(
  95. array($this, 'open'),
  96. array($this, 'close'),
  97. array($this, 'read'),
  98. array($this, 'sess_write'),
  99. array($this, 'destroy'),
  100. array($this, 'gc')
  101. );
  102. }
  103. /**
  104. * Wrapper for session_start()
  105. */
  106. public function start()
  107. {
  108. $this->start = microtime(true);
  109. $this->ip = rcube_utils::remote_addr();
  110. $this->logging = $this->config->get('log_session', false);
  111. $lifetime = $this->config->get('session_lifetime', 1) * 60;
  112. $this->set_lifetime($lifetime);
  113. session_start();
  114. }
  115. /**
  116. * Abstract methods should be implemented by driver classes
  117. */
  118. abstract function open($save_path, $session_name);
  119. abstract function close();
  120. abstract function destroy($key);
  121. abstract function read($key);
  122. abstract function write($key, $vars);
  123. abstract function update($key, $newvars, $oldvars);
  124. /**
  125. * session write handler. This calls the implementation methods for write/update after some initial checks.
  126. *
  127. * @param $key
  128. * @param $vars
  129. *
  130. * @return bool
  131. */
  132. public function sess_write($key, $vars)
  133. {
  134. if ($this->nowrite) {
  135. return true;
  136. }
  137. // check cache
  138. $oldvars = $this->get_cache($key);
  139. // if there are cached vars, update store, else insert new data
  140. if ($oldvars) {
  141. $newvars = $this->_fixvars($vars, $oldvars);
  142. return $this->update($key, $newvars, $oldvars);
  143. }
  144. else {
  145. return $this->write($key, $vars);
  146. }
  147. }
  148. /**
  149. * Wrapper for session_write_close()
  150. */
  151. public function write_close()
  152. {
  153. session_write_close();
  154. // write_close() is called on script shutdown, see rcube::shutdown()
  155. // execute cleanup functionality if enabled by session gc handler
  156. // we do this after closing the session for better performance
  157. $this->gc_shutdown();
  158. }
  159. /**
  160. * Creates a new (separate) session
  161. *
  162. * @param array Session data
  163. *
  164. * @return string Session identifier (on success)
  165. */
  166. public function create($data)
  167. {
  168. $length = strlen(session_id());
  169. $key = rcube_utils::random_bytes($length);
  170. // create new session
  171. if ($this->write($key, $this->serialize($data))) {
  172. return $key;
  173. }
  174. }
  175. /**
  176. * Merge vars with old vars and apply unsets
  177. */
  178. protected function _fixvars($vars, $oldvars)
  179. {
  180. if ($oldvars !== null) {
  181. $a_oldvars = $this->unserialize($oldvars);
  182. if (is_array($a_oldvars)) {
  183. // remove unset keys on oldvars
  184. foreach ((array)$this->unsets as $var) {
  185. if (isset($a_oldvars[$var])) {
  186. unset($a_oldvars[$var]);
  187. }
  188. else {
  189. $path = explode('.', $var);
  190. $k = array_pop($path);
  191. $node = &$this->get_node($path, $a_oldvars);
  192. unset($node[$k]);
  193. }
  194. }
  195. $newvars = $this->serialize(array_merge(
  196. (array)$a_oldvars, (array)$this->unserialize($vars)));
  197. }
  198. else {
  199. $newvars = $vars;
  200. }
  201. }
  202. $this->unsets = array();
  203. return $newvars;
  204. }
  205. /**
  206. * Execute registered garbage collector routines
  207. */
  208. public function gc($maxlifetime)
  209. {
  210. // move gc execution to the script shutdown function
  211. // see rcube::shutdown() and rcube_session::write_close()
  212. $this->gc_enabled = $maxlifetime;
  213. return true;
  214. }
  215. /**
  216. * Register additional garbage collector functions
  217. *
  218. * @param mixed Callback function
  219. */
  220. public function register_gc_handler($func)
  221. {
  222. foreach ($this->gc_handlers as $handler) {
  223. if ($handler == $func) {
  224. return;
  225. }
  226. }
  227. $this->gc_handlers[] = $func;
  228. }
  229. /**
  230. * Garbage collector handler to run on script shutdown
  231. */
  232. protected function gc_shutdown()
  233. {
  234. if ($this->gc_enabled) {
  235. foreach ($this->gc_handlers as $fct) {
  236. call_user_func($fct);
  237. }
  238. }
  239. }
  240. /**
  241. * Generate and set new session id
  242. *
  243. * @param boolean $destroy If enabled the current session will be destroyed
  244. * @return bool
  245. */
  246. public function regenerate_id($destroy=true)
  247. {
  248. session_regenerate_id($destroy);
  249. $this->vars = null;
  250. $this->key = session_id();
  251. return true;
  252. }
  253. /**
  254. * See if we have vars of this key already cached, and if so, return them.
  255. *
  256. * @param string $key Session ID
  257. *
  258. * @return string
  259. */
  260. protected function get_cache($key)
  261. {
  262. // no session data in cache (read() returns false)
  263. if (!$this->key) {
  264. $cache = null;
  265. }
  266. // use internal data for fast requests (up to 0.5 sec.)
  267. else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
  268. $cache = $this->vars;
  269. }
  270. else { // else read data again
  271. $cache = $this->read($key);
  272. }
  273. return $cache;
  274. }
  275. /**
  276. * Append the given value to the certain node in the session data array
  277. *
  278. * Warning: Do not use if you already modified $_SESSION in the same request (#1490608)
  279. *
  280. * @param string Path denoting the session variable where to append the value
  281. * @param string Key name under which to append the new value (use null for appending to an indexed list)
  282. * @param mixed Value to append to the session data array
  283. */
  284. public function append($path, $key, $value)
  285. {
  286. // re-read session data from DB because it might be outdated
  287. if (!$this->reloaded && microtime(true) - $this->start > 0.5) {
  288. $this->reload();
  289. $this->reloaded = true;
  290. $this->start = microtime(true);
  291. }
  292. $node = &$this->get_node(explode('.', $path), $_SESSION);
  293. if ($key !== null) {
  294. $node[$key] = $value;
  295. $path .= '.' . $key;
  296. }
  297. else {
  298. $node[] = $value;
  299. }
  300. $this->appends[] = $path;
  301. // when overwriting a previously unset variable
  302. if ($this->unsets[$path]) {
  303. unset($this->unsets[$path]);
  304. }
  305. }
  306. /**
  307. * Unset a session variable
  308. *
  309. * @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5)
  310. * @return boolean True on success
  311. */
  312. public function remove($var=null)
  313. {
  314. if (empty($var)) {
  315. return $this->destroy(session_id());
  316. }
  317. $this->unsets[] = $var;
  318. if (isset($_SESSION[$var])) {
  319. unset($_SESSION[$var]);
  320. }
  321. else {
  322. $path = explode('.', $var);
  323. $key = array_pop($path);
  324. $node = &$this->get_node($path, $_SESSION);
  325. unset($node[$key]);
  326. }
  327. return true;
  328. }
  329. /**
  330. * Kill this session
  331. */
  332. public function kill()
  333. {
  334. $this->vars = null;
  335. $this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
  336. $this->destroy(session_id());
  337. rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
  338. }
  339. /**
  340. * Re-read session data from storage backend
  341. */
  342. public function reload()
  343. {
  344. // collect updated data from previous appends
  345. $merge_data = array();
  346. foreach ((array)$this->appends as $var) {
  347. $path = explode('.', $var);
  348. $value = $this->get_node($path, $_SESSION);
  349. $k = array_pop($path);
  350. $node = &$this->get_node($path, $merge_data);
  351. $node[$k] = $value;
  352. }
  353. if ($this->key) {
  354. $data = $this->read($this->key);
  355. }
  356. if ($data) {
  357. session_decode($data);
  358. // apply appends and unsets to reloaded data
  359. $_SESSION = array_merge_recursive($_SESSION, $merge_data);
  360. foreach ((array)$this->unsets as $var) {
  361. if (isset($_SESSION[$var])) {
  362. unset($_SESSION[$var]);
  363. }
  364. else {
  365. $path = explode('.', $var);
  366. $k = array_pop($path);
  367. $node = &$this->get_node($path, $_SESSION);
  368. unset($node[$k]);
  369. }
  370. }
  371. }
  372. }
  373. /**
  374. * Returns a reference to the node in data array referenced by the given path.
  375. * e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments']
  376. */
  377. protected function &get_node($path, &$data_arr)
  378. {
  379. $node = &$data_arr;
  380. if (!empty($path)) {
  381. foreach ((array)$path as $key) {
  382. if (!isset($node[$key]))
  383. $node[$key] = array();
  384. $node = &$node[$key];
  385. }
  386. }
  387. return $node;
  388. }
  389. /**
  390. * Serialize session data
  391. */
  392. protected function serialize($vars)
  393. {
  394. $data = '';
  395. if (is_array($vars)) {
  396. foreach ($vars as $var=>$value)
  397. $data .= $var.'|'.serialize($value);
  398. }
  399. else {
  400. $data = 'b:0;';
  401. }
  402. return $data;
  403. }
  404. /**
  405. * Unserialize session data
  406. * http://www.php.net/manual/en/function.session-decode.php#56106
  407. */
  408. protected function unserialize($str)
  409. {
  410. $str = (string)$str;
  411. $endptr = strlen($str);
  412. $p = 0;
  413. $serialized = '';
  414. $items = 0;
  415. $level = 0;
  416. while ($p < $endptr) {
  417. $q = $p;
  418. while ($str[$q] != '|')
  419. if (++$q >= $endptr)
  420. break 2;
  421. if ($str[$p] == '!') {
  422. $p++;
  423. $has_value = false;
  424. }
  425. else {
  426. $has_value = true;
  427. }
  428. $name = substr($str, $p, $q - $p);
  429. $q++;
  430. $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
  431. if ($has_value) {
  432. for (;;) {
  433. $p = $q;
  434. switch (strtolower($str[$q])) {
  435. case 'n': // null
  436. case 'b': // boolean
  437. case 'i': // integer
  438. case 'd': // decimal
  439. do $q++;
  440. while ( ($q < $endptr) && ($str[$q] != ';') );
  441. $q++;
  442. $serialized .= substr($str, $p, $q - $p);
  443. if ($level == 0)
  444. break 2;
  445. break;
  446. case 'r': // reference
  447. $q+= 2;
  448. for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++)
  449. $id .= $str[$q];
  450. $q++;
  451. // increment pointer because of outer array
  452. $serialized .= 'R:' . ($id + 1) . ';';
  453. if ($level == 0)
  454. break 2;
  455. break;
  456. case 's': // string
  457. $q+=2;
  458. for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++)
  459. $length .= $str[$q];
  460. $q+=2;
  461. $q+= (int)$length + 2;
  462. $serialized .= substr($str, $p, $q - $p);
  463. if ($level == 0)
  464. break 2;
  465. break;
  466. case 'a': // array
  467. case 'o': // object
  468. do $q++;
  469. while ($q < $endptr && $str[$q] != '{');
  470. $q++;
  471. $level++;
  472. $serialized .= substr($str, $p, $q - $p);
  473. break;
  474. case '}': // end of array|object
  475. $q++;
  476. $serialized .= substr($str, $p, $q - $p);
  477. if (--$level == 0)
  478. break 2;
  479. break;
  480. default:
  481. return false;
  482. }
  483. }
  484. }
  485. else {
  486. $serialized .= 'N;';
  487. $q += 2;
  488. }
  489. $items++;
  490. $p = $q;
  491. }
  492. return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
  493. }
  494. /**
  495. * Setter for session lifetime
  496. */
  497. public function set_lifetime($lifetime)
  498. {
  499. $this->lifetime = max(120, $lifetime);
  500. // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
  501. $now = time();
  502. $this->now = $now - ($now % ($this->lifetime / 2));
  503. }
  504. /**
  505. * Getter for remote IP saved with this session
  506. */
  507. public function get_ip()
  508. {
  509. return $this->ip;
  510. }
  511. /**
  512. * Setter for cookie encryption secret
  513. */
  514. function set_secret($secret = null)
  515. {
  516. // generate random hash and store in session
  517. if (!$secret) {
  518. if (!empty($_SESSION['auth_secret'])) {
  519. $secret = $_SESSION['auth_secret'];
  520. }
  521. else {
  522. $secret = rcube_utils::random_bytes(strlen($this->key));
  523. }
  524. }
  525. $_SESSION['auth_secret'] = $secret;
  526. }
  527. /**
  528. * Enable/disable IP check
  529. */
  530. function set_ip_check($check)
  531. {
  532. $this->ip_check = $check;
  533. }
  534. /**
  535. * Setter for the cookie name used for session cookie
  536. */
  537. function set_cookiename($cookiename)
  538. {
  539. if ($cookiename) {
  540. $this->cookiename = $cookiename;
  541. }
  542. }
  543. /**
  544. * Check session authentication cookie
  545. *
  546. * @return boolean True if valid, False if not
  547. */
  548. function check_auth()
  549. {
  550. $this->cookie = $_COOKIE[$this->cookiename];
  551. $result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
  552. if (!$result) {
  553. $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
  554. }
  555. if ($result && $this->_mkcookie($this->now) != $this->cookie) {
  556. $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
  557. $result = false;
  558. // Check if using id from a previous time slot
  559. for ($i = 1; $i <= 2; $i++) {
  560. $prev = $this->now - ($this->lifetime / 2) * $i;
  561. if ($this->_mkcookie($prev) == $this->cookie) {
  562. $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
  563. $this->set_auth_cookie();
  564. $result = true;
  565. }
  566. }
  567. }
  568. if (!$result) {
  569. $this->log("Session authentication failed for " . $this->key
  570. . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
  571. }
  572. return $result;
  573. }
  574. /**
  575. * Set session authentication cookie
  576. */
  577. public function set_auth_cookie()
  578. {
  579. $this->cookie = $this->_mkcookie($this->now);
  580. rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
  581. $_COOKIE[$this->cookiename] = $this->cookie;
  582. }
  583. /**
  584. * Create session cookie for specified time slot.
  585. *
  586. * @param int Time slot to use
  587. *
  588. * @return string
  589. */
  590. protected function _mkcookie($timeslot)
  591. {
  592. // make sure the secret key exists
  593. $this->set_secret();
  594. // no need to hash this, it's just a random string
  595. return $_SESSION['auth_secret'] . '-' . $timeslot;
  596. }
  597. /**
  598. * Writes debug information to the log
  599. */
  600. function log($line)
  601. {
  602. if ($this->logging) {
  603. rcube::write_log('session', $line);
  604. }
  605. }
  606. }