source: github/program/include/rcube_session.php @ c73efcc

HEADcourier-fixdev-browser-capabilitiespdorelease-0.8
Last change on this file since c73efcc was c73efcc, checked in by thomascube <thomas@…>, 15 months ago

Reset IP stored in session when destroying session data (#1488056)

  • Property mode set to 100644
File size: 15.4 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_session.php                                     |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
9 | Copyright (C) 2011, Kolab Systems AG                                  |
10 |                                                                       |
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.                     |
14 |                                                                       |
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
27/**
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 */
34class rcube_session
35{
36  private $db;
37  private $ip;
38  private $start;
39  private $changed;
40  private $unsets = array();
41  private $gc_handlers = array();
42  private $cookiename = 'roundcube_sessauth';
43  private $vars = false;
44  private $key;
45  private $now;
46  private $prev;
47  private $secret = '';
48  private $ip_check = false;
49  private $logging = false;
50  private $keep_alive = 0;
51  private $memcache;
52
53  /**
54   * Default constructor
55   */
56  public function __construct($db, $config)
57  {
58    $this->db      = $db;
59    $this->start   = microtime(true);
60    $this->ip      = $_SERVER['REMOTE_ADDR'];
61    $this->logging = $config->get('log_session', false);
62
63    $lifetime = $config->get('session_lifetime', 1) * 60;
64    $this->set_lifetime($lifetime);
65
66    // use memcache backend
67    if ($config->get('session_storage', 'db') == 'memcache') {
68      $this->memcache = rcmail::get_instance()->get_memcache();
69
70      // set custom functions for PHP session management if memcache is available
71      if ($this->memcache) {
72        session_set_save_handler(
73          array($this, 'open'),
74          array($this, 'close'),
75          array($this, 'mc_read'),
76          array($this, 'mc_write'),
77          array($this, 'mc_destroy'),
78          array($this, 'gc'));
79      }
80      else {
81        raise_error(array('code' => 604, 'type' => 'db',
82          'line' => __LINE__, 'file' => __FILE__,
83          'message' => "Failed to connect to memcached. Please check configuration"),
84          true, true);
85      }
86    }
87    else {
88      // set custom functions for PHP session management
89      session_set_save_handler(
90        array($this, 'open'),
91        array($this, 'close'),
92        array($this, 'db_read'),
93        array($this, 'db_write'),
94        array($this, 'db_destroy'),
95        array($this, 'db_gc'));
96      }
97  }
98
99
100  public function open($save_path, $session_name)
101  {
102    return true;
103  }
104
105
106  public function close()
107  {
108    return true;
109  }
110
111
112  /**
113   * Delete session data for the given key
114   *
115   * @param string Session ID
116   */
117  public function destroy($key)
118  {
119    return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key);
120  }
121
122
123  /**
124   * Read session data from database
125   *
126   * @param string Session ID
127   * @return string Session vars
128   */
129  public function db_read($key)
130  {
131    $sql_result = $this->db->query(
132      "SELECT vars, ip, changed FROM ".get_table_name('session')
133      ." WHERE sess_id = ?", $key);
134
135    if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
136      $this->changed = strtotime($sql_arr['changed']);
137      $this->ip      = $sql_arr['ip'];
138      $this->vars    = base64_decode($sql_arr['vars']);
139      $this->key     = $key;
140
141      if (!empty($this->vars))
142        return $this->vars;
143    }
144
145    return false;
146  }
147
148
149  /**
150   * Save session data.
151   * handler for session_read()
152   *
153   * @param string Session ID
154   * @param string Serialized session vars
155   * @return boolean True on success
156   */
157  public function db_write($key, $vars)
158  {
159    $ts = microtime(true);
160    $now = $this->db->fromunixtime((int)$ts);
161
162    // no session row in DB (db_read() returns false)
163    if (!$this->key) {
164      $oldvars = false;
165    }
166    // use internal data from read() for fast requests (up to 0.5 sec.)
167    else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
168      $oldvars = $this->vars;
169    }
170    else { // else read data again from DB
171      $oldvars = $this->db_read($key);
172    }
173
174    if ($oldvars !== false) {
175      $newvars = $this->_fixvars($vars, $oldvars);
176
177      if ($newvars !== $oldvars) {
178        $this->db->query(
179          sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
180            get_table_name('session'), $now),
181          base64_encode($newvars), $key);
182      }
183      else if ($ts - $this->changed > $this->lifetime / 2) {
184        $this->db->query("UPDATE ".get_table_name('session')." SET changed=$now WHERE sess_id=?", $key);
185      }
186    }
187    else {
188      $this->db->query(
189        sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
190          "VALUES (?, ?, ?, %s, %s)",
191          get_table_name('session'), $now, $now),
192        $key, base64_encode($vars), (string)$this->ip);
193    }
194
195    return true;
196  }
197
198
199  /**
200   * Merge vars with old vars and apply unsets
201   */
202  private function _fixvars($vars, $oldvars)
203  {
204    if ($oldvars !== false) {
205      $a_oldvars = $this->unserialize($oldvars);
206      if (is_array($a_oldvars)) {
207        foreach ((array)$this->unsets as $k)
208          unset($a_oldvars[$k]);
209
210        $newvars = $this->serialize(array_merge(
211          (array)$a_oldvars, (array)$this->unserialize($vars)));
212      }
213      else
214        $newvars = $vars;
215    }
216
217    $this->unsets = array();
218    return $newvars;
219  }
220
221
222  /**
223   * Handler for session_destroy()
224   *
225   * @param string Session ID
226   * @return boolean True on success
227   */
228  public function db_destroy($key)
229  {
230    $this->db->query(
231      sprintf("DELETE FROM %s WHERE sess_id = ?", get_table_name('session')),
232      $key);
233
234    return true;
235  }
236
237
238  /**
239   * Garbage collecting function
240   *
241   * @param string Session lifetime in seconds
242   * @return boolean True on success
243   */
244  public function db_gc($maxlifetime)
245  {
246    // just delete all expired sessions
247    $this->db->query(
248      sprintf("DELETE FROM %s WHERE changed < %s",
249        get_table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
250
251    $this->gc();
252
253    return true;
254  }
255
256
257  /**
258   * Read session data from memcache
259   *
260   * @param string Session ID
261   * @return string Session vars
262   */
263  public function mc_read($key)
264  {
265    if ($value = $this->memcache->get($key)) {
266      $arr = unserialize($value);
267      $this->changed = $arr['changed'];
268      $this->ip      = $arr['ip'];
269      $this->vars    = $arr['vars'];
270      $this->key     = $key;
271
272      if (!empty($this->vars))
273        return $this->vars;
274    }
275
276    return false;
277  }
278
279  /**
280   * Save session data.
281   * handler for session_read()
282   *
283   * @param string Session ID
284   * @param string Serialized session vars
285   * @return boolean True on success
286   */
287  public function mc_write($key, $vars)
288  {
289    $ts = microtime(true);
290
291    // no session data in cache (mc_read() returns false)
292    if (!$this->key)
293      $oldvars = false;
294    // use internal data for fast requests (up to 0.5 sec.)
295    else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
296      $oldvars = $this->vars;
297    else // else read data again
298      $oldvars = $this->mc_read($key);
299
300    $newvars = $oldvars !== false ? $this->_fixvars($vars, $oldvars) : $vars;
301   
302    if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2)
303      return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)), MEMCACHE_COMPRESSED, $this->lifetime);
304   
305    return true;
306  }
307
308  /**
309   * Handler for session_destroy() with memcache backend
310   *
311   * @param string Session ID
312   * @return boolean True on success
313   */
314  public function mc_destroy($key)
315  {
316    return $this->memcache->delete($key);
317  }
318
319
320  /**
321   * Execute registered garbage collector routines
322   */
323  public function gc()
324  {
325    foreach ($this->gc_handlers as $fct)
326      call_user_func($fct);
327  }
328
329
330  /**
331   * Register additional garbage collector functions
332   *
333   * @param mixed Callback function
334   */
335  public function register_gc_handler($func)
336  {
337    foreach ($this->gc_handlers as $handler) {
338      if ($handler == $func) {
339        return;
340      }
341    }
342
343    $this->gc_handlers[] = $func;
344  }
345
346
347  /**
348   * Generate and set new session id
349   *
350   * @param boolean $destroy If enabled the current session will be destroyed
351   */
352  public function regenerate_id($destroy=true)
353  {
354    session_regenerate_id($destroy);
355
356    $this->vars = false;
357    $this->key  = session_id();
358
359    return true;
360  }
361
362
363  /**
364   * Unset a session variable
365   *
366   * @param string Varibale name
367   * @return boolean True on success
368   */
369  public function remove($var=null)
370  {
371    if (empty($var))
372      return $this->destroy(session_id());
373
374    $this->unsets[] = $var;
375    unset($_SESSION[$var]);
376
377    return true;
378  }
379 
380  /**
381   * Kill this session
382   */
383  public function kill()
384  {
385    $this->vars = false;
386    $this->ip = $_SERVER['REMOTE_ADDR']; // update IP (might have changed)
387    $this->destroy(session_id());
388    rcmail::setcookie($this->cookiename, '-del-', time() - 60);
389  }
390
391
392  /**
393   * Re-read session data from storage backend
394   */
395  public function reload()
396  {
397    if ($this->key && $this->memcache)
398      $data = $this->mc_read($this->key);
399    else if ($this->key)
400      $data = $this->db_read($this->key);
401
402    if ($data)
403     session_decode($data);
404  }
405
406
407  /**
408   * Serialize session data
409   */
410  private function serialize($vars)
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
422  /**
423   * Unserialize session data
424   * http://www.php.net/manual/en/function.session-decode.php#56106
425   */
426  private function unserialize($str)
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
510
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      $this->prev = $this->now - ($this->lifetime / 2);
522  }
523
524  /**
525   * Setter for keep_alive interval
526   */
527  public function set_keep_alive($keep_alive)
528  {
529    $this->keep_alive = $keep_alive;
530   
531    if ($this->lifetime < $keep_alive)
532        $this->set_lifetime($keep_alive + 30);
533  }
534
535  /**
536   * Getter for keep_alive interval
537   */
538  public function get_keep_alive()
539  {
540    return $this->keep_alive;
541  }
542
543  /**
544   * Getter for remote IP saved with this session
545   */
546  public function get_ip()
547  {
548    return $this->ip;
549  }
550 
551  /**
552   * Setter for cookie encryption secret
553   */
554  function set_secret($secret)
555  {
556    $this->secret = $secret;
557  }
558
559
560  /**
561   * Enable/disable IP check
562   */
563  function set_ip_check($check)
564  {
565    $this->ip_check = $check;
566  }
567 
568  /**
569   * Setter for the cookie name used for session cookie
570   */
571  function set_cookiename($cookiename)
572  {
573    if ($cookiename)
574      $this->cookiename = $cookiename;
575  }
576
577
578  /**
579   * Check session authentication cookie
580   *
581   * @return boolean True if valid, False if not
582   */
583  function check_auth()
584  {
585    $this->cookie = $_COOKIE[$this->cookiename];
586    $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
587
588    if (!$result)
589      $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']);
590
591    if ($result && $this->_mkcookie($this->now) != $this->cookie) {
592      // Check if using id from previous time slot
593      if ($this->_mkcookie($this->prev) == $this->cookie) {
594        $this->set_auth_cookie();
595      }
596      else {
597        $result = false;
598        $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent");
599      }
600    }
601
602    return $result;
603  }
604
605
606  /**
607   * Set session authentication cookie
608   */
609  function set_auth_cookie()
610  {
611    $this->cookie = $this->_mkcookie($this->now);
612    rcmail::setcookie($this->cookiename, $this->cookie, 0);
613    $_COOKIE[$this->cookiename] = $this->cookie;
614  }
615
616
617  /**
618   * Create session cookie from session data
619   *
620   * @param int Time slot to use
621   */
622  function _mkcookie($timeslot)
623  {
624    $auth_string = "$this->key,$this->secret,$timeslot";
625    return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
626  }
627 
628  /**
629   *
630   */
631  function log($line)
632  {
633    if ($this->logging)
634      write_log('session', $line);
635  }
636
637}
Note: See TracBrowser for help on using the repository browser.