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