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 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  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. * Merge vars with old vars and apply unsets
  161. */
  162. protected function _fixvars($vars, $oldvars)
  163. {
  164. if ($oldvars !== null) {
  165. $a_oldvars = $this->unserialize($oldvars);
  166. if (is_array($a_oldvars)) {
  167. // remove unset keys on oldvars
  168. foreach ((array)$this->unsets as $var) {
  169. if (isset($a_oldvars[$var])) {
  170. unset($a_oldvars[$var]);
  171. }
  172. else {
  173. $path = explode('.', $var);
  174. $k = array_pop($path);
  175. $node = &$this->get_node($path, $a_oldvars);
  176. unset($node[$k]);
  177. }
  178. }
  179. $newvars = $this->serialize(array_merge(
  180. (array)$a_oldvars, (array)$this->unserialize($vars)));
  181. }
  182. else {
  183. $newvars = $vars;
  184. }
  185. }
  186. $this->unsets = array();
  187. return $newvars;
  188. }
  189. /**
  190. * Execute registered garbage collector routines
  191. */
  192. public function gc($maxlifetime)
  193. {
  194. // move gc execution to the script shutdown function
  195. // see rcube::shutdown() and rcube_session::write_close()
  196. $this->gc_enabled = $maxlifetime;
  197. return true;
  198. }
  199. /**
  200. * Register additional garbage collector functions
  201. *
  202. * @param mixed Callback function
  203. */
  204. public function register_gc_handler($func)
  205. {
  206. foreach ($this->gc_handlers as $handler) {
  207. if ($handler == $func) {
  208. return;
  209. }
  210. }
  211. $this->gc_handlers[] = $func;
  212. }
  213. /**
  214. * Garbage collector handler to run on script shutdown
  215. */
  216. protected function gc_shutdown()
  217. {
  218. if ($this->gc_enabled) {
  219. foreach ($this->gc_handlers as $fct) {
  220. call_user_func($fct);
  221. }
  222. }
  223. }
  224. /**
  225. * Generate and set new session id
  226. *
  227. * @param boolean $destroy If enabled the current session will be destroyed
  228. * @return bool
  229. */
  230. public function regenerate_id($destroy=true)
  231. {
  232. session_regenerate_id($destroy);
  233. $this->vars = null;
  234. $this->key = session_id();
  235. return true;
  236. }
  237. /**
  238. * See if we have vars of this key already cached, and if so, return them.
  239. *
  240. * @param string $key Session ID
  241. *
  242. * @return string
  243. */
  244. protected function get_cache($key)
  245. {
  246. // no session data in cache (read() returns false)
  247. if (!$this->key) {
  248. $cache = null;
  249. }
  250. // use internal data for fast requests (up to 0.5 sec.)
  251. else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
  252. $cache = $this->vars;
  253. }
  254. else { // else read data again
  255. $cache = $this->read($key);
  256. }
  257. return $cache;
  258. }
  259. /**
  260. * Append the given value to the certain node in the session data array
  261. *
  262. * Warning: Do not use if you already modified $_SESSION in the same request (#1490608)
  263. *
  264. * @param string Path denoting the session variable where to append the value
  265. * @param string Key name under which to append the new value (use null for appending to an indexed list)
  266. * @param mixed Value to append to the session data array
  267. */
  268. public function append($path, $key, $value)
  269. {
  270. // re-read session data from DB because it might be outdated
  271. if (!$this->reloaded && microtime(true) - $this->start > 0.5) {
  272. $this->reload();
  273. $this->reloaded = true;
  274. $this->start = microtime(true);
  275. }
  276. $node = &$this->get_node(explode('.', $path), $_SESSION);
  277. if ($key !== null) {
  278. $node[$key] = $value;
  279. $path .= '.' . $key;
  280. }
  281. else {
  282. $node[] = $value;
  283. }
  284. $this->appends[] = $path;
  285. // when overwriting a previously unset variable
  286. if ($this->unsets[$path]) {
  287. unset($this->unsets[$path]);
  288. }
  289. }
  290. /**
  291. * Unset a session variable
  292. *
  293. * @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5)
  294. * @return boolean True on success
  295. */
  296. public function remove($var=null)
  297. {
  298. if (empty($var)) {
  299. return $this->destroy(session_id());
  300. }
  301. $this->unsets[] = $var;
  302. if (isset($_SESSION[$var])) {
  303. unset($_SESSION[$var]);
  304. }
  305. else {
  306. $path = explode('.', $var);
  307. $key = array_pop($path);
  308. $node = &$this->get_node($path, $_SESSION);
  309. unset($node[$key]);
  310. }
  311. return true;
  312. }
  313. /**
  314. * Kill this session
  315. */
  316. public function kill()
  317. {
  318. $this->vars = null;
  319. $this->ip = rcube_utils::remote_addr(); // update IP (might have changed)
  320. $this->destroy(session_id());
  321. rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
  322. }
  323. /**
  324. * Re-read session data from storage backend
  325. */
  326. public function reload()
  327. {
  328. // collect updated data from previous appends
  329. $merge_data = array();
  330. foreach ((array)$this->appends as $var) {
  331. $path = explode('.', $var);
  332. $value = $this->get_node($path, $_SESSION);
  333. $k = array_pop($path);
  334. $node = &$this->get_node($path, $merge_data);
  335. $node[$k] = $value;
  336. }
  337. if ($this->key) {
  338. $data = $this->read($this->key);
  339. }
  340. if ($data) {
  341. session_decode($data);
  342. // apply appends and unsets to reloaded data
  343. $_SESSION = array_merge_recursive($_SESSION, $merge_data);
  344. foreach ((array)$this->unsets as $var) {
  345. if (isset($_SESSION[$var])) {
  346. unset($_SESSION[$var]);
  347. }
  348. else {
  349. $path = explode('.', $var);
  350. $k = array_pop($path);
  351. $node = &$this->get_node($path, $_SESSION);
  352. unset($node[$k]);
  353. }
  354. }
  355. }
  356. }
  357. /**
  358. * Returns a reference to the node in data array referenced by the given path.
  359. * e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments']
  360. */
  361. protected function &get_node($path, &$data_arr)
  362. {
  363. $node = &$data_arr;
  364. if (!empty($path)) {
  365. foreach ((array)$path as $key) {
  366. if (!isset($node[$key]))
  367. $node[$key] = array();
  368. $node = &$node[$key];
  369. }
  370. }
  371. return $node;
  372. }
  373. /**
  374. * Serialize session data
  375. */
  376. protected function serialize($vars)
  377. {
  378. $data = '';
  379. if (is_array($vars)) {
  380. foreach ($vars as $var=>$value)
  381. $data .= $var.'|'.serialize($value);
  382. }
  383. else {
  384. $data = 'b:0;';
  385. }
  386. return $data;
  387. }
  388. /**
  389. * Unserialize session data
  390. * http://www.php.net/manual/en/function.session-decode.php#56106
  391. */
  392. protected function unserialize($str)
  393. {
  394. $str = (string)$str;
  395. $endptr = strlen($str);
  396. $p = 0;
  397. $serialized = '';
  398. $items = 0;
  399. $level = 0;
  400. while ($p < $endptr) {
  401. $q = $p;
  402. while ($str[$q] != '|')
  403. if (++$q >= $endptr)
  404. break 2;
  405. if ($str[$p] == '!') {
  406. $p++;
  407. $has_value = false;
  408. }
  409. else {
  410. $has_value = true;
  411. }
  412. $name = substr($str, $p, $q - $p);
  413. $q++;
  414. $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
  415. if ($has_value) {
  416. for (;;) {
  417. $p = $q;
  418. switch (strtolower($str[$q])) {
  419. case 'n': // null
  420. case 'b': // boolean
  421. case 'i': // integer
  422. case 'd': // decimal
  423. do $q++;
  424. while ( ($q < $endptr) && ($str[$q] != ';') );
  425. $q++;
  426. $serialized .= substr($str, $p, $q - $p);
  427. if ($level == 0)
  428. break 2;
  429. break;
  430. case 'r': // reference
  431. $q+= 2;
  432. for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++)
  433. $id .= $str[$q];
  434. $q++;
  435. // increment pointer because of outer array
  436. $serialized .= 'R:' . ($id + 1) . ';';
  437. if ($level == 0)
  438. break 2;
  439. break;
  440. case 's': // string
  441. $q+=2;
  442. for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++)
  443. $length .= $str[$q];
  444. $q+=2;
  445. $q+= (int)$length + 2;
  446. $serialized .= substr($str, $p, $q - $p);
  447. if ($level == 0)
  448. break 2;
  449. break;
  450. case 'a': // array
  451. case 'o': // object
  452. do $q++;
  453. while ($q < $endptr && $str[$q] != '{');
  454. $q++;
  455. $level++;
  456. $serialized .= substr($str, $p, $q - $p);
  457. break;
  458. case '}': // end of array|object
  459. $q++;
  460. $serialized .= substr($str, $p, $q - $p);
  461. if (--$level == 0)
  462. break 2;
  463. break;
  464. default:
  465. return false;
  466. }
  467. }
  468. }
  469. else {
  470. $serialized .= 'N;';
  471. $q += 2;
  472. }
  473. $items++;
  474. $p = $q;
  475. }
  476. return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
  477. }
  478. /**
  479. * Setter for session lifetime
  480. */
  481. public function set_lifetime($lifetime)
  482. {
  483. $this->lifetime = max(120, $lifetime);
  484. // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
  485. $now = time();
  486. $this->now = $now - ($now % ($this->lifetime / 2));
  487. }
  488. /**
  489. * Getter for remote IP saved with this session
  490. */
  491. public function get_ip()
  492. {
  493. return $this->ip;
  494. }
  495. /**
  496. * Setter for cookie encryption secret
  497. */
  498. function set_secret($secret = null)
  499. {
  500. // generate random hash and store in session
  501. if (!$secret) {
  502. if (!empty($_SESSION['auth_secret'])) {
  503. $secret = $_SESSION['auth_secret'];
  504. }
  505. else {
  506. $secret = rcube_utils::random_bytes(strlen($this->key));
  507. }
  508. }
  509. $_SESSION['auth_secret'] = $secret;
  510. }
  511. /**
  512. * Enable/disable IP check
  513. */
  514. function set_ip_check($check)
  515. {
  516. $this->ip_check = $check;
  517. }
  518. /**
  519. * Setter for the cookie name used for session cookie
  520. */
  521. function set_cookiename($cookiename)
  522. {
  523. if ($cookiename) {
  524. $this->cookiename = $cookiename;
  525. }
  526. }
  527. /**
  528. * Check session authentication cookie
  529. *
  530. * @return boolean True if valid, False if not
  531. */
  532. function check_auth()
  533. {
  534. $this->cookie = $_COOKIE[$this->cookiename];
  535. $result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true;
  536. if (!$result) {
  537. $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr());
  538. }
  539. if ($result && $this->_mkcookie($this->now) != $this->cookie) {
  540. $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
  541. $result = false;
  542. // Check if using id from a previous time slot
  543. for ($i = 1; $i <= 2; $i++) {
  544. $prev = $this->now - ($this->lifetime / 2) * $i;
  545. if ($this->_mkcookie($prev) == $this->cookie) {
  546. $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
  547. $this->set_auth_cookie();
  548. $result = true;
  549. }
  550. }
  551. }
  552. if (!$result) {
  553. $this->log("Session authentication failed for " . $this->key
  554. . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
  555. }
  556. return $result;
  557. }
  558. /**
  559. * Set session authentication cookie
  560. */
  561. public function set_auth_cookie()
  562. {
  563. $this->cookie = $this->_mkcookie($this->now);
  564. rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
  565. $_COOKIE[$this->cookiename] = $this->cookie;
  566. }
  567. /**
  568. * Create session cookie for specified time slot.
  569. *
  570. * @param int Time slot to use
  571. *
  572. * @return string
  573. */
  574. protected function _mkcookie($timeslot)
  575. {
  576. // make sure the secret key exists
  577. $this->set_secret();
  578. // no need to hash this, it's just a random string
  579. return $_SESSION['auth_secret'] . '-' . $timeslot;
  580. }
  581. /**
  582. * Writes debug information to the log
  583. */
  584. function log($line)
  585. {
  586. if ($this->logging) {
  587. rcube::write_log('session', $line);
  588. }
  589. }
  590. }