1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Memcached based session handler. 19 * 20 * @package core 21 * @copyright 2013 Petr Skoda {@link http://skodak.org} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core\session; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Memcached based session handler. 31 * 32 * @package core 33 * @copyright 2013 Petr Skoda {@link http://skodak.org} 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class memcached extends handler { 37 /** @var string $savepath save_path string */ 38 protected $savepath; 39 40 /** @var array $servers list of servers parsed from save_path */ 41 protected $servers; 42 43 /** @var string $prefix session key prefix */ 44 protected $prefix; 45 46 /** @var int $acquiretimeout how long to wait for session lock */ 47 protected $acquiretimeout = 120; 48 49 /** 50 * @var int $lockexpire how long to wait before expiring the lock so that other requests 51 * may continue execution, ignored if PECL memcached is below version 2.2.0. 52 */ 53 protected $lockexpire = 7200; 54 55 /** 56 * @var integer $lockretrysleep Used for memcached 3.x (PHP7), the amount of time to 57 * sleep between attempts to acquire the session lock. Mimics the deprecated config 58 * memcached.sess_lock_wait. 59 */ 60 protected $lockretrysleep = 150; 61 62 /** 63 * Create new instance of handler. 64 */ 65 public function __construct() { 66 global $CFG; 67 68 if (empty($CFG->session_memcached_save_path)) { 69 $this->savepath = ''; 70 } else { 71 $this->savepath = $CFG->session_memcached_save_path; 72 } 73 74 if (empty($this->savepath)) { 75 $this->servers = array(); 76 } else { 77 $this->servers = self::connection_string_to_memcache_servers($this->savepath); 78 } 79 80 if (empty($CFG->session_memcached_prefix)) { 81 $this->prefix = ini_get('memcached.sess_prefix'); 82 } else { 83 $this->prefix = $CFG->session_memcached_prefix; 84 } 85 86 if (!empty($CFG->session_memcached_acquire_lock_timeout)) { 87 $this->acquiretimeout = (int)$CFG->session_memcached_acquire_lock_timeout; 88 } 89 90 if (!empty($CFG->session_memcached_lock_expire)) { 91 $this->lockexpire = (int)$CFG->session_memcached_lock_expire; 92 } 93 94 if (!empty($CFG->session_memcached_lock_retry_sleep)) { 95 $this->lockretrysleep = (int)$CFG->session_memcached_lock_retry_sleep; 96 } 97 } 98 99 /** 100 * Start the session. 101 * @return bool success 102 */ 103 public function start() { 104 ini_set('memcached.sess_locking', $this->requires_write_lock() ? '1' : '0'); 105 106 // NOTE: memcached before 2.2.0 expires session locks automatically after max_execution_time, 107 // this leads to major difference compared to other session drivers that timeout 108 // and stop the second request execution instead. 109 110 $default = ini_get('max_execution_time'); 111 set_time_limit($this->acquiretimeout); 112 113 $isnewsession = empty($_COOKIE[session_name()]); 114 $starttimer = microtime(true); 115 116 $result = parent::start(); 117 118 // If session_start returned TRUE, but it took as long 119 // as the timeout value, and the $_SESSION returned is 120 // empty when should not have been (isnewsession false) 121 // then assume it did timeout and is invalid. 122 // Add 1 second to elapsed time to account for inexact 123 // timings in php_memcached_session.c. 124 // @TODO Remove this check when php-memcached is fixed 125 // to return false after key lock acquisition timeout. 126 if (!$isnewsession && $result && count($_SESSION) == 0 127 && (microtime(true) - $starttimer + 1) >= floatval($this->acquiretimeout)) { 128 $result = false; 129 } 130 131 set_time_limit($default); 132 return $result; 133 } 134 135 /** 136 * Init session handler. 137 */ 138 public function init() { 139 if (!extension_loaded('memcached')) { 140 throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension is not loaded'); 141 } 142 $version = phpversion('memcached'); 143 if (!$version or version_compare($version, '2.0') < 0) { 144 throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension version must be at least 2.0'); 145 } 146 if (empty($this->savepath)) { 147 throw new exception('sessionhandlerproblem', 'error', '', null, '$CFG->session_memcached_save_path must be specified in config.php'); 148 } 149 150 ini_set('session.save_handler', 'memcached'); 151 ini_set('session.save_path', $this->savepath); 152 ini_set('memcached.sess_prefix', $this->prefix); 153 ini_set('memcached.sess_lock_expire', $this->lockexpire); 154 155 if (version_compare($version, '3.0.0-dev') >= 0) { 156 // With memcached 3.x (PHP 7) we configure the max retries to make and the time to sleep between each retry. 157 // There are two sleep config values, an initial and a max value. 158 // After each attempt the memcached module adjusts the sleep value to be the lesser of the configured max 159 // value, or 2X the previous value. 160 // With default memcached.ini configs (5, 1s, 2s) the result is only 5 attempts to lock over 9 sec. 161 // To mimic the behavior of the 2.2.x module so we get more attempts and much more frequently, config both 162 // sleep values to the old default value of 150 msec (making it constant) and calculate number of retries 163 // using the existing Moodle config $CFG->session_memcached_acquire_lock_timeout. 164 // Doing this so admins configure session lock attempt timeout in familiar terms, and more straight-forward 165 // to detect if lock attempt timeout has occurred in start(). 166 // If _min and _max values are not equal, the actual lock acquire timeout will not be the expected 167 // configured value in $CFG->session_memcached_acquire_lock_timeout; this will cause session data loss when 168 // failure to acquire the lock is not detected. 169 ini_set('memcached.sess_lock_wait_min', $this->lockretrysleep); 170 ini_set('memcached.sess_lock_wait_max', $this->lockretrysleep); 171 ini_set('memcached.sess_lock_retries', (int)(($this->acquiretimeout * 1000) / $this->lockretrysleep) + 1); 172 } else { 173 // With memcached 2.2.x we configure max time to attempt lock, and accept default value (in memcached.ini) 174 // for sleep time between each attempt (usually 150 msec), then memcached calculates the max number of 175 // retries to make. 176 ini_set('memcached.sess_lock_max_wait', $this->acquiretimeout); 177 } 178 179 } 180 181 /** 182 * Check the backend contains data for this session id. 183 * 184 * Note: this is intended to be called from manager::session_exists() only. 185 * 186 * @param string $sid 187 * @return bool true if session found. 188 */ 189 public function session_exists($sid) { 190 if (!$this->servers) { 191 return false; 192 } 193 194 // Go through the list of all servers because 195 // we do not know where the session handler put the 196 // data. 197 198 foreach ($this->servers as $server) { 199 list($host, $port) = $server; 200 $memcached = new \Memcached(); 201 $memcached->addServer($host, $port); 202 $value = $memcached->get($this->prefix . $sid); 203 $memcached->quit(); 204 if ($value !== false) { 205 return true; 206 } 207 } 208 209 return false; 210 } 211 212 /** 213 * Kill all active sessions, the core sessions table is 214 * purged afterwards. 215 */ 216 public function kill_all_sessions() { 217 global $DB; 218 if (!$this->servers) { 219 return; 220 } 221 222 // Go through the list of all servers because 223 // we do not know where the session handler put the 224 // data. 225 226 $memcacheds = array(); 227 foreach ($this->servers as $server) { 228 list($host, $port) = $server; 229 $memcached = new \Memcached(); 230 $memcached->addServer($host, $port); 231 $memcacheds[] = $memcached; 232 } 233 234 // Note: this can be significantly improved by fetching keys from memcached, 235 // but we need to make sure we are not deleting somebody else's sessions. 236 237 $rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid'); 238 foreach ($rs as $record) { 239 foreach ($memcacheds as $memcached) { 240 $memcached->delete($this->prefix . $record->sid); 241 } 242 } 243 $rs->close(); 244 245 foreach ($memcacheds as $memcached) { 246 $memcached->quit(); 247 } 248 } 249 250 /** 251 * Kill one session, the session record is removed afterwards. 252 * @param string $sid 253 */ 254 public function kill_session($sid) { 255 if (!$this->servers) { 256 return; 257 } 258 259 // Go through the list of all servers because 260 // we do not know where the session handler put the 261 // data. 262 263 foreach ($this->servers as $server) { 264 list($host, $port) = $server; 265 $memcached = new \Memcached(); 266 $memcached->addServer($host, $port); 267 $memcached->delete($this->prefix . $sid); 268 $memcached->quit(); 269 } 270 } 271 272 /** 273 * Convert a connection string to an array of servers. 274 * 275 * "abc:123, xyz:789" to 276 * [ 277 * ['abc', '123'], 278 * ['xyz', '789'], 279 * ] 280 * 281 * @param string $str save_path value containing memcached connection string 282 * @return array[] 283 */ 284 protected static function connection_string_to_memcache_servers(string $str) : array { 285 $servers = []; 286 $parts = explode(',', $str); 287 foreach ($parts as $part) { 288 $part = trim($part); 289 $pos = strrpos($part, ':'); 290 if ($pos !== false) { 291 $host = substr($part, 0, $pos); 292 $port = substr($part, ($pos + 1)); 293 } else { 294 $host = $part; 295 $port = 11211; 296 } 297 $servers[] = [$host, $port]; 298 } 299 return $servers; 300 } 301 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body