PageRenderTime 48ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/program/include/rcube_session.php

https://github.com/netconstructor/roundcubemail
PHP | 632 lines | 371 code | 100 blank | 161 comment | 88 complexity | 339e105576ff9ad699d3401c9a6ff262 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcube_session.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2005-2012, The Roundcube Dev Team |
  8. | Copyright (C) 2011, Kolab Systems AG |
  9. | |
  10. | Licensed under the GNU General Public License version 3 or |
  11. | any later version with exceptions for skins & plugins. |
  12. | See the README file for a full license statement. |
  13. | |
  14. | PURPOSE: |
  15. | Provide database supported session management |
  16. | |
  17. +-----------------------------------------------------------------------+
  18. | Author: Thomas Bruederli <roundcube@gmail.com> |
  19. | Author: Aleksander Machniak <alec@alec.pl> |
  20. +-----------------------------------------------------------------------+
  21. */
  22. /**
  23. * Class to provide database supported session storage
  24. *
  25. * @package Framework
  26. * @subpackage Core
  27. * @author Thomas Bruederli <roundcube@gmail.com>
  28. * @author Aleksander Machniak <alec@alec.pl>
  29. */
  30. class rcube_session
  31. {
  32. private $db;
  33. private $ip;
  34. private $start;
  35. private $changed;
  36. private $unsets = array();
  37. private $gc_handlers = array();
  38. private $cookiename = 'roundcube_sessauth';
  39. private $vars;
  40. private $key;
  41. private $now;
  42. private $secret = '';
  43. private $ip_check = false;
  44. private $logging = false;
  45. private $memcache;
  46. /**
  47. * Default constructor
  48. */
  49. public function __construct($db, $config)
  50. {
  51. $this->db = $db;
  52. $this->start = microtime(true);
  53. $this->ip = $_SERVER['REMOTE_ADDR'];
  54. $this->logging = $config->get('log_session', false);
  55. $lifetime = $config->get('session_lifetime', 1) * 60;
  56. $this->set_lifetime($lifetime);
  57. // use memcache backend
  58. if ($config->get('session_storage', 'db') == 'memcache') {
  59. $this->memcache = rcube::get_instance()->get_memcache();
  60. // set custom functions for PHP session management if memcache is available
  61. if ($this->memcache) {
  62. session_set_save_handler(
  63. array($this, 'open'),
  64. array($this, 'close'),
  65. array($this, 'mc_read'),
  66. array($this, 'mc_write'),
  67. array($this, 'mc_destroy'),
  68. array($this, 'gc'));
  69. }
  70. else {
  71. rcube::raise_error(array('code' => 604, 'type' => 'db',
  72. 'line' => __LINE__, 'file' => __FILE__,
  73. 'message' => "Failed to connect to memcached. Please check configuration"),
  74. true, true);
  75. }
  76. }
  77. else {
  78. // set custom functions for PHP session management
  79. session_set_save_handler(
  80. array($this, 'open'),
  81. array($this, 'close'),
  82. array($this, 'db_read'),
  83. array($this, 'db_write'),
  84. array($this, 'db_destroy'),
  85. array($this, 'db_gc'));
  86. }
  87. }
  88. public function open($save_path, $session_name)
  89. {
  90. return true;
  91. }
  92. public function close()
  93. {
  94. return true;
  95. }
  96. /**
  97. * Delete session data for the given key
  98. *
  99. * @param string Session ID
  100. */
  101. public function destroy($key)
  102. {
  103. return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key);
  104. }
  105. /**
  106. * Read session data from database
  107. *
  108. * @param string Session ID
  109. * @return string Session vars
  110. */
  111. public function db_read($key)
  112. {
  113. $sql_result = $this->db->query(
  114. "SELECT vars, ip, changed FROM ".$this->db->table_name('session')
  115. ." WHERE sess_id = ?", $key);
  116. if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
  117. $this->changed = strtotime($sql_arr['changed']);
  118. $this->ip = $sql_arr['ip'];
  119. $this->vars = base64_decode($sql_arr['vars']);
  120. $this->key = $key;
  121. return !empty($this->vars) ? (string) $this->vars : '';
  122. }
  123. return null;
  124. }
  125. /**
  126. * Save session data.
  127. * handler for session_read()
  128. *
  129. * @param string Session ID
  130. * @param string Serialized session vars
  131. * @return boolean True on success
  132. */
  133. public function db_write($key, $vars)
  134. {
  135. $ts = microtime(true);
  136. $now = $this->db->fromunixtime((int)$ts);
  137. // no session row in DB (db_read() returns false)
  138. if (!$this->key) {
  139. $oldvars = null;
  140. }
  141. // use internal data from read() for fast requests (up to 0.5 sec.)
  142. else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
  143. $oldvars = $this->vars;
  144. }
  145. else { // else read data again from DB
  146. $oldvars = $this->db_read($key);
  147. }
  148. if ($oldvars !== null) {
  149. $newvars = $this->_fixvars($vars, $oldvars);
  150. if ($newvars !== $oldvars) {
  151. $this->db->query(
  152. sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
  153. $this->db->table_name('session'), $now),
  154. base64_encode($newvars), $key);
  155. }
  156. else if ($ts - $this->changed > $this->lifetime / 2) {
  157. $this->db->query("UPDATE ".$this->db->table_name('session')." SET changed=$now WHERE sess_id=?", $key);
  158. }
  159. }
  160. else {
  161. $this->db->query(
  162. sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
  163. "VALUES (?, ?, ?, %s, %s)",
  164. $this->db->table_name('session'), $now, $now),
  165. $key, base64_encode($vars), (string)$this->ip);
  166. }
  167. return true;
  168. }
  169. /**
  170. * Merge vars with old vars and apply unsets
  171. */
  172. private function _fixvars($vars, $oldvars)
  173. {
  174. if ($oldvars !== null) {
  175. $a_oldvars = $this->unserialize($oldvars);
  176. if (is_array($a_oldvars)) {
  177. foreach ((array)$this->unsets as $k)
  178. unset($a_oldvars[$k]);
  179. $newvars = $this->serialize(array_merge(
  180. (array)$a_oldvars, (array)$this->unserialize($vars)));
  181. }
  182. else
  183. $newvars = $vars;
  184. }
  185. $this->unsets = array();
  186. return $newvars;
  187. }
  188. /**
  189. * Handler for session_destroy()
  190. *
  191. * @param string Session ID
  192. *
  193. * @return boolean True on success
  194. */
  195. public function db_destroy($key)
  196. {
  197. if ($key) {
  198. $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?", $this->db->table_name('session')), $key);
  199. }
  200. return true;
  201. }
  202. /**
  203. * Garbage collecting function
  204. *
  205. * @param string Session lifetime in seconds
  206. * @return boolean True on success
  207. */
  208. public function db_gc($maxlifetime)
  209. {
  210. // just delete all expired sessions
  211. $this->db->query(
  212. sprintf("DELETE FROM %s WHERE changed < %s",
  213. $this->db->table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
  214. $this->gc();
  215. return true;
  216. }
  217. /**
  218. * Read session data from memcache
  219. *
  220. * @param string Session ID
  221. * @return string Session vars
  222. */
  223. public function mc_read($key)
  224. {
  225. if ($value = $this->memcache->get($key)) {
  226. $arr = unserialize($value);
  227. $this->changed = $arr['changed'];
  228. $this->ip = $arr['ip'];
  229. $this->vars = $arr['vars'];
  230. $this->key = $key;
  231. return !empty($this->vars) ? (string) $this->vars : '';
  232. }
  233. return null;
  234. }
  235. /**
  236. * Save session data.
  237. * handler for session_read()
  238. *
  239. * @param string Session ID
  240. * @param string Serialized session vars
  241. * @return boolean True on success
  242. */
  243. public function mc_write($key, $vars)
  244. {
  245. $ts = microtime(true);
  246. // no session data in cache (mc_read() returns false)
  247. if (!$this->key)
  248. $oldvars = null;
  249. // use internal data for fast requests (up to 0.5 sec.)
  250. else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
  251. $oldvars = $this->vars;
  252. else // else read data again
  253. $oldvars = $this->mc_read($key);
  254. $newvars = $oldvars !== null ? $this->_fixvars($vars, $oldvars) : $vars;
  255. if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2)
  256. return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)), MEMCACHE_COMPRESSED, $this->lifetime);
  257. return true;
  258. }
  259. /**
  260. * Handler for session_destroy() with memcache backend
  261. *
  262. * @param string Session ID
  263. *
  264. * @return boolean True on success
  265. */
  266. public function mc_destroy($key)
  267. {
  268. if ($key) {
  269. // #1488592: use 2nd argument
  270. $this->memcache->delete($key, 0);
  271. }
  272. return true;
  273. }
  274. /**
  275. * Execute registered garbage collector routines
  276. */
  277. public function gc()
  278. {
  279. foreach ($this->gc_handlers as $fct) {
  280. call_user_func($fct);
  281. }
  282. }
  283. /**
  284. * Register additional garbage collector functions
  285. *
  286. * @param mixed Callback function
  287. */
  288. public function register_gc_handler($func)
  289. {
  290. foreach ($this->gc_handlers as $handler) {
  291. if ($handler == $func) {
  292. return;
  293. }
  294. }
  295. $this->gc_handlers[] = $func;
  296. }
  297. /**
  298. * Generate and set new session id
  299. *
  300. * @param boolean $destroy If enabled the current session will be destroyed
  301. */
  302. public function regenerate_id($destroy=true)
  303. {
  304. session_regenerate_id($destroy);
  305. $this->vars = null;
  306. $this->key = session_id();
  307. return true;
  308. }
  309. /**
  310. * Unset a session variable
  311. *
  312. * @param string Varibale name
  313. * @return boolean True on success
  314. */
  315. public function remove($var=null)
  316. {
  317. if (empty($var))
  318. return $this->destroy(session_id());
  319. $this->unsets[] = $var;
  320. unset($_SESSION[$var]);
  321. return true;
  322. }
  323. /**
  324. * Kill this session
  325. */
  326. public function kill()
  327. {
  328. $this->vars = null;
  329. $this->ip = $_SERVER['REMOTE_ADDR']; // update IP (might have changed)
  330. $this->destroy(session_id());
  331. rcube_utils::setcookie($this->cookiename, '-del-', time() - 60);
  332. }
  333. /**
  334. * Re-read session data from storage backend
  335. */
  336. public function reload()
  337. {
  338. if ($this->key && $this->memcache)
  339. $data = $this->mc_read($this->key);
  340. else if ($this->key)
  341. $data = $this->db_read($this->key);
  342. if ($data)
  343. session_decode($data);
  344. }
  345. /**
  346. * Serialize session data
  347. */
  348. private function serialize($vars)
  349. {
  350. $data = '';
  351. if (is_array($vars))
  352. foreach ($vars as $var=>$value)
  353. $data .= $var.'|'.serialize($value);
  354. else
  355. $data = 'b:0;';
  356. return $data;
  357. }
  358. /**
  359. * Unserialize session data
  360. * http://www.php.net/manual/en/function.session-decode.php#56106
  361. */
  362. private function unserialize($str)
  363. {
  364. $str = (string)$str;
  365. $endptr = strlen($str);
  366. $p = 0;
  367. $serialized = '';
  368. $items = 0;
  369. $level = 0;
  370. while ($p < $endptr) {
  371. $q = $p;
  372. while ($str[$q] != '|')
  373. if (++$q >= $endptr) break 2;
  374. if ($str[$p] == '!') {
  375. $p++;
  376. $has_value = false;
  377. } else {
  378. $has_value = true;
  379. }
  380. $name = substr($str, $p, $q - $p);
  381. $q++;
  382. $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
  383. if ($has_value) {
  384. for (;;) {
  385. $p = $q;
  386. switch (strtolower($str[$q])) {
  387. case 'n': /* null */
  388. case 'b': /* boolean */
  389. case 'i': /* integer */
  390. case 'd': /* decimal */
  391. do $q++;
  392. while ( ($q < $endptr) && ($str[$q] != ';') );
  393. $q++;
  394. $serialized .= substr($str, $p, $q - $p);
  395. if ($level == 0) break 2;
  396. break;
  397. case 'r': /* reference */
  398. $q+= 2;
  399. for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
  400. $q++;
  401. $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */
  402. if ($level == 0) break 2;
  403. break;
  404. case 's': /* string */
  405. $q+=2;
  406. for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
  407. $q+=2;
  408. $q+= (int)$length + 2;
  409. $serialized .= substr($str, $p, $q - $p);
  410. if ($level == 0) break 2;
  411. break;
  412. case 'a': /* array */
  413. case 'o': /* object */
  414. do $q++;
  415. while ( ($q < $endptr) && ($str[$q] != '{') );
  416. $q++;
  417. $level++;
  418. $serialized .= substr($str, $p, $q - $p);
  419. break;
  420. case '}': /* end of array|object */
  421. $q++;
  422. $serialized .= substr($str, $p, $q - $p);
  423. if (--$level == 0) break 2;
  424. break;
  425. default:
  426. return false;
  427. }
  428. }
  429. } else {
  430. $serialized .= 'N;';
  431. $q += 2;
  432. }
  433. $items++;
  434. $p = $q;
  435. }
  436. return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
  437. }
  438. /**
  439. * Setter for session lifetime
  440. */
  441. public function set_lifetime($lifetime)
  442. {
  443. $this->lifetime = max(120, $lifetime);
  444. // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
  445. $now = time();
  446. $this->now = $now - ($now % ($this->lifetime / 2));
  447. }
  448. /**
  449. * Getter for remote IP saved with this session
  450. */
  451. public function get_ip()
  452. {
  453. return $this->ip;
  454. }
  455. /**
  456. * Setter for cookie encryption secret
  457. */
  458. function set_secret($secret)
  459. {
  460. $this->secret = $secret;
  461. }
  462. /**
  463. * Enable/disable IP check
  464. */
  465. function set_ip_check($check)
  466. {
  467. $this->ip_check = $check;
  468. }
  469. /**
  470. * Setter for the cookie name used for session cookie
  471. */
  472. function set_cookiename($cookiename)
  473. {
  474. if ($cookiename)
  475. $this->cookiename = $cookiename;
  476. }
  477. /**
  478. * Check session authentication cookie
  479. *
  480. * @return boolean True if valid, False if not
  481. */
  482. function check_auth()
  483. {
  484. $this->cookie = $_COOKIE[$this->cookiename];
  485. $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
  486. if (!$result)
  487. $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']);
  488. if ($result && $this->_mkcookie($this->now) != $this->cookie) {
  489. $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now));
  490. $result = false;
  491. // Check if using id from a previous time slot
  492. for ($i = 1; $i <= 2; $i++) {
  493. $prev = $this->now - ($this->lifetime / 2) * $i;
  494. if ($this->_mkcookie($prev) == $this->cookie) {
  495. $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie);
  496. $this->set_auth_cookie();
  497. $result = true;
  498. }
  499. }
  500. }
  501. if (!$result)
  502. $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev));
  503. return $result;
  504. }
  505. /**
  506. * Set session authentication cookie
  507. */
  508. function set_auth_cookie()
  509. {
  510. $this->cookie = $this->_mkcookie($this->now);
  511. rcube_utils::setcookie($this->cookiename, $this->cookie, 0);
  512. $_COOKIE[$this->cookiename] = $this->cookie;
  513. }
  514. /**
  515. * Create session cookie from session data
  516. *
  517. * @param int Time slot to use
  518. */
  519. function _mkcookie($timeslot)
  520. {
  521. $auth_string = "$this->key,$this->secret,$timeslot";
  522. return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
  523. }
  524. /**
  525. * Writes debug information to the log
  526. */
  527. function log($line)
  528. {
  529. if ($this->logging)
  530. rcube::write_log('session', $line);
  531. }
  532. }