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