session: use osTicket\Session Backends
This commit should have probably been broken into smaller commits but the wholesale change was required since it's an overhaul of how osTicket handles sessions.
This commit is contained in:
@@ -15,14 +15,20 @@
|
||||
**********************************************************************/
|
||||
file_exists('../main.inc.php') or die('System Error');
|
||||
|
||||
/*
|
||||
* Why API_SESSION ??
|
||||
* It indicates that the session is API - which session handler should handle as
|
||||
* stateless for new sessions.
|
||||
* Existing session continue to work as expected - this it's important for
|
||||
* SSO authentication, which uses /api/auth/* endpoints. Such calls are not
|
||||
* stateless.
|
||||
*
|
||||
*/
|
||||
define('API_SESSION', true);
|
||||
|
||||
// APICALL const.
|
||||
define('APICALL', true);
|
||||
|
||||
// Disable sessions for the API. API should be considered stateless and
|
||||
// shouldn't chew up database records to store sessions
|
||||
if (!defined('DISABLE_SESSION'))
|
||||
define('DISABLE_SESSION', true);
|
||||
|
||||
require_once('../main.inc.php');
|
||||
require_once(INCLUDE_DIR.'class.http.php');
|
||||
require_once(INCLUDE_DIR.'class.api.php');
|
||||
|
||||
@@ -13,15 +13,9 @@
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
// Use sessions — it's important for SSO authentication, which uses
|
||||
// /api/auth/ext
|
||||
define('DISABLE_SESSION', false);
|
||||
|
||||
require 'api.inc.php';
|
||||
|
||||
# Include the main api urls
|
||||
require_once INCLUDE_DIR."class.dispatcher.php";
|
||||
|
||||
$dispatcher = patterns('',
|
||||
url_post("^/tickets\.(?P<format>xml|json|email)$", array('api.tickets.php:TicketApiController','create')),
|
||||
url('^/tasks/', patterns('',
|
||||
@@ -29,8 +23,8 @@ $dispatcher = patterns('',
|
||||
))
|
||||
);
|
||||
|
||||
// Send api signal so backend can register endpoints
|
||||
Signal::send('api', $dispatcher);
|
||||
|
||||
# Call the respective function
|
||||
print $dispatcher->resolve(Osticket::get_path_info());
|
||||
?>
|
||||
|
||||
@@ -79,6 +79,8 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'"
|
||||
|
||||
/* Client specific defaults */
|
||||
define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT);
|
||||
define('SESSION_MAXLIFE', $thisclient ? $thisclient->getMaxIdleTime() :
|
||||
SESSION_TTL);
|
||||
|
||||
require(INCLUDE_DIR.'class.nav.php');
|
||||
$nav = new UserNav($thisclient, 'home');
|
||||
|
||||
@@ -59,9 +59,7 @@ class osTicket {
|
||||
require_once(INCLUDE_DIR.'class.company.php');
|
||||
// Load the config
|
||||
$this->config = new OsticketConfig();
|
||||
// Start session (if not disabled)
|
||||
if (!defined('DISABLE_SESSION') || !DISABLE_SESSION)
|
||||
$this->session = osTicketSession::start(SESSION_TTL,
|
||||
$this->session = osTicketSession::start(SESSION_TTL,
|
||||
$this->isUpgradePending());
|
||||
// CSRF Token
|
||||
$this->csrf = new CSRF('__CSRFToken__');
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/*********************************************************************
|
||||
class.ostsession.php
|
||||
|
||||
Custom osTicket session handler.
|
||||
osTicket Session Management Backend
|
||||
|
||||
Peter Rotich <peter@osticket.com>
|
||||
Copyright (c) 2006-2013 osTicket
|
||||
Copyright (c) 2022 osTicket
|
||||
http://www.osticket.com
|
||||
|
||||
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
||||
@@ -13,89 +13,222 @@
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
include_once INCLUDE_DIR.'class.session.php';
|
||||
|
||||
class osTicketSession {
|
||||
static $backends = array(
|
||||
'db' => 'DbSessionBackend',
|
||||
'memcache' => 'MemcacheSessionBackend',
|
||||
'noop' => 'NoopSessionBackend',
|
||||
'system' => 'FallbackSessionBackend',
|
||||
);
|
||||
// Session Name
|
||||
private static $name = 'OSTSESSID';
|
||||
// Session Backend instance
|
||||
private $backend;
|
||||
// Session default TTL
|
||||
private $ttl;
|
||||
|
||||
var $ttl = SESSION_TTL;
|
||||
var $data = '';
|
||||
var $data_hash = '';
|
||||
var $id = '';
|
||||
var $backend;
|
||||
function __construct($ttl = SESSION_TTL, $checkdbversion = false) {
|
||||
// Session ttl cannot exceed php.ini maxlifetime setting
|
||||
$maxlife = ini_get('session.gc_maxlifetime');
|
||||
$this->ttl = min($ttl ?: ($maxlife ?: SESSION_TTL), $maxlife);
|
||||
|
||||
function __construct($ttl=0, $checkdbversion=false){
|
||||
$this->ttl = $ttl ?: ini_get('session.gc_maxlifetime') ?: SESSION_TTL;
|
||||
|
||||
// Set osTicket specific session name.
|
||||
session_name('OSTSESSID');
|
||||
|
||||
// Forced cleanup on shutdown
|
||||
register_shutdown_function('session_write_close');
|
||||
|
||||
// Set session cleanup time to match TTL
|
||||
ini_set('session.gc_maxlifetime', $ttl);
|
||||
|
||||
// Skip db version check if version is later than 1.7
|
||||
if ((!defined('MAJOR_VERSION') || $checkdbversion)
|
||||
&& OsticketConfig::getDBVersion())
|
||||
return session_start();
|
||||
|
||||
# Cookies
|
||||
// Avoid setting a cookie domain without a dot, thanks
|
||||
// http://stackoverflow.com/a/1188145
|
||||
$domain = null;
|
||||
if (isset($_SERVER['HTTP_HOST'])
|
||||
&& strpos($_SERVER['HTTP_HOST'], '.') !== false
|
||||
&& !Validator::is_ip($_SERVER['HTTP_HOST']))
|
||||
// Remote port specification, as it will make an invalid domain
|
||||
list($domain) = explode(':', $_SERVER['HTTP_HOST']);
|
||||
|
||||
session_set_cookie_params($ttl, ROOT_PATH, $domain,
|
||||
// Set osTicket specific session name
|
||||
// TODO: Make it configurable in ost-config.php
|
||||
session_name(self::$name);
|
||||
// Set Default cookie Params before we start the session
|
||||
session_set_cookie_params($this->ttl, ROOT_PATH, Http::domain(),
|
||||
osTicket::is_https(), true);
|
||||
|
||||
// Determine Session Backend to use
|
||||
$bk = 'db';
|
||||
if (defined('DISABLE_SESSION'))
|
||||
$bk = 'noop';
|
||||
elseif (defined('SESSION_BACKEND'))
|
||||
$bk = SESSION_BACKEND;
|
||||
/** Determine Session Backend to use **/
|
||||
if ((!defined('MAJOR_VERSION') || $checkdbversion)
|
||||
&& OsticketConfig::getDBVersion())
|
||||
// Default PHP SessionHandler
|
||||
$bk = 'system';
|
||||
else // default is database
|
||||
$bk = self::session_backend() ?: 'database';
|
||||
|
||||
// Tag the backend with storage backend class
|
||||
switch ($bk) {
|
||||
case 'database':
|
||||
$bk = "$bk:DatabaseSessionStorageBackend";
|
||||
break;
|
||||
case 'memcache':
|
||||
$bk = "$bk:MemcacheSessionStorageBackend";
|
||||
break;
|
||||
case 'memcache:database':
|
||||
// memcache is primary while database is secondary
|
||||
// Database doesn't store data when set as secondary, but
|
||||
// very useful when session data is offloaded to memcache with
|
||||
// database tracking sessions ids and expire time for the
|
||||
// purpose of knowing who is online.
|
||||
$bk = sprintf('%s:%s:%s',
|
||||
'memcache',
|
||||
'MemcacheSessionStorageBackend',
|
||||
'DatabaseSessionStorageBackend');
|
||||
break;
|
||||
case 'database:memcache':
|
||||
// database is primary while memcache is secondary
|
||||
// This setup only makes sense if memcache is being used as
|
||||
// backup backend
|
||||
$bk = sprintf('%s:%s:%s',
|
||||
'database',
|
||||
'DatabaseSessionStorageBackend',
|
||||
'MemcacheSessionStorageBackend');
|
||||
break;
|
||||
case 'system':
|
||||
case 'noop':
|
||||
// system & noop don't require storage backends
|
||||
break;
|
||||
default:
|
||||
// Assume invalid entry - default to system
|
||||
$bk = 'system';
|
||||
}
|
||||
|
||||
/** Session Backend options **/
|
||||
$options = [
|
||||
// Default TTL (maxlife)
|
||||
'session_ttl' => $this->ttl,
|
||||
// It indicates that the session is API session - which session
|
||||
// handler should handle as stateless for new sessions.
|
||||
'api_session' => defined('API_SESSION'),
|
||||
];
|
||||
|
||||
// Set MaxLifeTime if defined. This is defined per user so it's
|
||||
// preferred over ttl.
|
||||
if (defined('SESSION_MAXLIFE') && is_numeric(SESSION_MAXLIFE))
|
||||
$options['session_maxlife'] = SESSION_MAXLIFE;
|
||||
|
||||
// If $bk doesn't exit then an Exception will be thrown
|
||||
// Backend is object is turned on success
|
||||
try {
|
||||
if (!isset(self::$backends[$bk])
|
||||
|| !class_exists(self::$backends[$bk]))
|
||||
$bk = 'db';
|
||||
$this->backend = new self::$backends[$bk]($this->ttl);
|
||||
}
|
||||
catch (Exception $x) {
|
||||
// Use the database for sessions
|
||||
trigger_error($x->getMessage(), E_USER_WARNING);
|
||||
$this->backend = new self::$backends['db']($this->ttl);
|
||||
// If $bk doesn't exit then an Exception will be thrown
|
||||
// backend object is turned on success
|
||||
$this->backend = osTicket\Session\SessionBackend::register($bk,
|
||||
$options, true);
|
||||
} catch (Throwable $t) {
|
||||
die($t->getMessage());
|
||||
// We're just gonna default to php session handler and hope for
|
||||
// the best.
|
||||
// TODO: Log the error and perhaps rethrow the exception so it
|
||||
// can be fatal?
|
||||
}
|
||||
|
||||
if ($this->backend instanceof SessionBackend) {
|
||||
// Set handlers.
|
||||
session_set_save_handler(
|
||||
array($this->backend, 'open'),
|
||||
array($this->backend, 'close'),
|
||||
array($this->backend, 'read'),
|
||||
array($this->backend, 'write'),
|
||||
array($this->backend, 'destroy'),
|
||||
array($this->backend, 'gc')
|
||||
);
|
||||
}
|
||||
|
||||
// Start the session.
|
||||
// Finally start the damn session.
|
||||
session_start();
|
||||
}
|
||||
|
||||
function regenerate_id(){
|
||||
$oldId = session_id();
|
||||
session_regenerate_id();
|
||||
$this->backend->destroy($oldId);
|
||||
/*
|
||||
* session_backend
|
||||
*
|
||||
* Get configured session backend if any.
|
||||
*/
|
||||
static function session_backend($default = false) {
|
||||
// Session Disabled?
|
||||
// This is useful when fetching logos or on CLI - we don't want to update the
|
||||
// session in such cases.
|
||||
if (defined('NOOP_SESSION') || defined('DISABLE_SESSION'))
|
||||
return 'noop'; // Ignore session data
|
||||
|
||||
// No session backend set - return default
|
||||
if (!defined('SESSION_BACKEND'))
|
||||
return $default;
|
||||
|
||||
// Explode backend incase it's chained
|
||||
list($bk, $secondary) = explode(':', SESSION_BACKEND);
|
||||
// Only recongnize supported primary backends
|
||||
switch (strtolower($bk)) {
|
||||
case 'memcache':
|
||||
// Make sure we have memcache servers defined - if not
|
||||
// then break so we can return default.
|
||||
// TODO: Log an error or throw an exception
|
||||
if (!defined('MEMCACHE_SERVERS'))
|
||||
break;
|
||||
// No break on purpose
|
||||
case 'database':
|
||||
case 'system':
|
||||
return strtolower(SESSION_BACKEND);
|
||||
break;
|
||||
default:
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* registered_backend
|
||||
*
|
||||
* get registered backend if any
|
||||
*/
|
||||
static function registered_backend() {
|
||||
global $ost;
|
||||
if ($ost && isset($ost->session))
|
||||
return $ost->session->backend;
|
||||
}
|
||||
|
||||
/*
|
||||
* regenerate($ttl)
|
||||
*
|
||||
* Regenerate current session_id and expire the old one in $ttl seconds
|
||||
* This is preferred over destroying the session immediately just incase
|
||||
* we have pending ajax requests for example
|
||||
*
|
||||
*/
|
||||
static function regenerate(int $ttl=60) {
|
||||
// Make sure session is active and headers are not already sent
|
||||
if (session_status() !== PHP_SESSION_ACTIVE
|
||||
|| headers_sent())
|
||||
return false;
|
||||
|
||||
// Save current ($old) session id
|
||||
$old = session_id();
|
||||
// Expire current session cookie now + ttl
|
||||
// We have to do it here before re regenerate becase renewCookie has
|
||||
// hardcoded session calls to get name & id
|
||||
// FIXME: Maybe bae...
|
||||
self::renewCookie(time(), $ttl);
|
||||
// Regenerate the session
|
||||
// TODO: use session_create_id() instead of session_regenerate_id -
|
||||
// but PHP (even 8) is still bugy and inconsistent when it comes to
|
||||
// using SessionIdInterface::create_sid depening on session settings.
|
||||
session_regenerate_id(false);
|
||||
// get a new session id and force commit
|
||||
// Expire old session now + ttl
|
||||
session_write_close();
|
||||
// Expire old session now + ttl
|
||||
// ::expire() is not a standard session routine so we have to commit
|
||||
// the current "new" session first
|
||||
self::expire($old, $ttl);
|
||||
// Restart the new session
|
||||
session_start();
|
||||
// Return the new session id
|
||||
$new = session_id();
|
||||
error_log("regenerate: old($old) new($new)");
|
||||
return session_id();
|
||||
}
|
||||
|
||||
/*
|
||||
* expire session
|
||||
*
|
||||
* Expire session in $ttl from now if the storage backend supports
|
||||
* it otherwise it should destroy it by calling this->destroy($id)
|
||||
*/
|
||||
// Expire session - end is near mb!
|
||||
static function expire($id, $ttl) {
|
||||
// See if we have a backend to ask to expire the session - otherwise
|
||||
// we destroy session now!
|
||||
if (!($backend=self::registered_backend()))
|
||||
return false;
|
||||
|
||||
// Expire session soonish (now() + $ttl) - end is near mb!
|
||||
return (bool) $backend->expire($id, $ttl);
|
||||
}
|
||||
|
||||
static function renewCookie($baseTime=false, $window=false) {
|
||||
$ttl = $window ?: SESSION_TTL;
|
||||
$expire = ($baseTime ?: time()) + $ttl;
|
||||
setcookie(session_name(), session_id(), $expire,
|
||||
ini_get('session.cookie_path'),
|
||||
ini_get('session.cookie_domain'),
|
||||
ini_get('session.cookie_secure'),
|
||||
ini_get('session.cookie_httponly'));
|
||||
// Trigger expire update - neeed for secondary handlers that only
|
||||
// log new sessions
|
||||
self::expire(session_id(), $ttl);
|
||||
}
|
||||
|
||||
static function destroyCookie() {
|
||||
@@ -106,270 +239,380 @@ class osTicketSession {
|
||||
ini_get('session.cookie_httponly'));
|
||||
}
|
||||
|
||||
static function renewCookie($baseTime=false, $window=false) {
|
||||
setcookie(session_name(), session_id(),
|
||||
($baseTime ?: time()) + ($window ?: SESSION_TTL),
|
||||
ini_get('session.cookie_path'),
|
||||
ini_get('session.cookie_domain'),
|
||||
ini_get('session.cookie_secure'),
|
||||
ini_get('session.cookie_httponly'));
|
||||
}
|
||||
|
||||
/* helper functions */
|
||||
|
||||
function get_online_users($sec=0){
|
||||
$sql='SELECT user_id FROM '.SESSION_TABLE.' WHERE user_id>0 AND session_expire>NOW()';
|
||||
if($sec)
|
||||
$sql.=" AND TIME_TO_SEC(TIMEDIFF(NOW(),session_updated))<$sec";
|
||||
|
||||
$users=array();
|
||||
if(($res=db_query($sql)) && db_num_rows($res)) {
|
||||
while(list($uid)=db_fetch_row($res))
|
||||
$users[] = $uid;
|
||||
}
|
||||
|
||||
static function get_online_users(int $seconds = 0) {
|
||||
// Authoretative is lookup is DatabaseSessionRecords assuming
|
||||
// database is the primary backend or secondary logger
|
||||
$records = DatabaseSessionRecord::active_sessions([
|
||||
'lastseen' => $seconds,
|
||||
'authenticated' => true,
|
||||
]);
|
||||
$users = [];
|
||||
foreach ($records as $record)
|
||||
$users[] = $record->getUserId();
|
||||
return $users;
|
||||
}
|
||||
|
||||
/* ---------- static function ---------- */
|
||||
static function start($ttl=0, $checkdbversion=false) {
|
||||
return new static($ttl, $checkdbversion);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SessionBackend implements SessionHandlerInterface {
|
||||
var $isnew = false;
|
||||
var $ttl;
|
||||
|
||||
function __construct($ttl=SESSION_TTL) {
|
||||
$this->ttl = $ttl;
|
||||
}
|
||||
|
||||
function open($save_path, $session_name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function getTTL() {
|
||||
return $this->ttl;
|
||||
}
|
||||
|
||||
function write($id, $data) {
|
||||
// Last chance session update
|
||||
$i = new ArrayObject(array('touched' => false));
|
||||
Signal::send('session.close', null, $i);
|
||||
return $this->update($id, $i['touched'] ? session_encode() : $data);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
$this->gc(0);
|
||||
}
|
||||
|
||||
abstract function read($id);
|
||||
abstract function update($id, $data);
|
||||
abstract function destroy($id);
|
||||
abstract function gc($maxlife);
|
||||
}
|
||||
|
||||
class SessionData
|
||||
extends VerySimpleModel {
|
||||
/*
|
||||
* DatabaseSessionRecord
|
||||
*
|
||||
* ORM hook to session table with SessionRecordInterface implementation
|
||||
*/
|
||||
class DatabaseSessionRecord extends VerySimpleModel
|
||||
implements osTicket\Session\SessionRecordInterface {
|
||||
static $meta = array(
|
||||
'table' => SESSION_TABLE,
|
||||
'pk' => array('session_id'),
|
||||
);
|
||||
}
|
||||
|
||||
class DbSessionBackend
|
||||
extends SessionBackend {
|
||||
var $data = null;
|
||||
public function isNew() {
|
||||
return $this->__new__;
|
||||
}
|
||||
|
||||
function read($id) {
|
||||
public function isValid() {
|
||||
return true;
|
||||
// Basic checks to make sure the session data is valid
|
||||
// TODO: Throw specific exceptions so it can be handled downstream
|
||||
if (isset($this->user_agent)
|
||||
&& strcmp($_SERVER['HTTP_USER_AGENT'], $this->user_agent) !== 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setId(string $id) {
|
||||
$this->session_id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->session_id;
|
||||
}
|
||||
|
||||
public function setData(string $data = null) {
|
||||
$this->session_data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
return $this->session_data;
|
||||
}
|
||||
|
||||
public function getExpireTime() {
|
||||
return $this->session_expire;
|
||||
}
|
||||
|
||||
private function setExpire(int $maxlife) {
|
||||
$this->session_expire = SqlFunction::NOW()
|
||||
->plus(SqlInterval::SECOND($maxlife));
|
||||
}
|
||||
|
||||
public function setTTL($ttl) {
|
||||
$this->setExpire($ttl);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function expire(int $ttl) {
|
||||
$this->setTTL($ttl);
|
||||
// Assume it will expire shortly - clear user_id
|
||||
$this->user_id = 0;
|
||||
return $this->commit();
|
||||
}
|
||||
|
||||
public function getUpdateTime() {
|
||||
return $this->session_updated;
|
||||
}
|
||||
|
||||
public function commit() {
|
||||
return ($this->save());
|
||||
}
|
||||
|
||||
public function save($refresh=false) {
|
||||
global $thisstaff;
|
||||
// See if we need to set the user id - should only be set once onlogin
|
||||
if (!$this->user_id && $thisstaff)
|
||||
$this->user_id = $thisstaff->getId();
|
||||
if (count($this->dirty))
|
||||
$this->session_updated = SqlFunction::NOW();
|
||||
return parent::save($refresh);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
return (parent::delete());
|
||||
}
|
||||
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'data' => $this->getData(),
|
||||
'updated' => $this->getUpdateTime(),
|
||||
'expires' => $this->getExpireTime(),
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* lookupRecord
|
||||
*
|
||||
* Given session id - lookup the record
|
||||
*
|
||||
* Possible returns;
|
||||
* false: on lookup failure
|
||||
* null: doesn't exists and autocreate is false
|
||||
* record: fetched or newly created session record
|
||||
*
|
||||
*/
|
||||
public static function lookupRecord($id, $autocreate = false,
|
||||
$backend=null) {
|
||||
// We're doing lookup locally so we can auto-create one if the
|
||||
// session doesn't exist.
|
||||
$record = false;
|
||||
try {
|
||||
$this->data = SessionData::objects()
|
||||
$record = self::objects()
|
||||
->filter(['session_id' => $id])
|
||||
->annotate(array('is_expired' =>
|
||||
new SqlExpr(new Q(array('session_expire__lt' => SqlFunction::NOW())))))
|
||||
->one();
|
||||
|
||||
if ($this->data->is_expired > 0) {
|
||||
if ($record->is_expired > 0) {
|
||||
// session_expire is in the past. Pretend it is expired and
|
||||
// reset the data. This will assist with CSRF issues
|
||||
$this->data->session_data='';
|
||||
$record->session_data = '';
|
||||
}
|
||||
$this->id = $id;
|
||||
}
|
||||
catch (DoesNotExist $e) {
|
||||
$this->data = new SessionData(['session_id' => $id, 'session_data' => '']);
|
||||
// We're auto-creating model (unsaved) when one doesn't exist?
|
||||
$record = $autocreate ? self::create($id) : null;
|
||||
}
|
||||
catch (OrmException $e) {
|
||||
return false;
|
||||
catch (OrmException | Exception $ex) {
|
||||
// This could happen if more than one record exits in the
|
||||
// database for example.
|
||||
}
|
||||
// Verify the User Agent string
|
||||
if (isset($this->data->user_agent)
|
||||
&& (strcmp($_SERVER['HTTP_USER_AGENT'], $this->data->user_agent) !== 0)) {
|
||||
$this->destroy($id);
|
||||
return false;
|
||||
}
|
||||
return $this->data->session_data;
|
||||
return $record;
|
||||
}
|
||||
|
||||
function update($id, $data){
|
||||
global $thisstaff;
|
||||
static function create($vars) {
|
||||
// We expect ::init($id) or ::init(array $vars)
|
||||
if ($vars && !is_array($vars))
|
||||
$vars = ['session_id' => $vars];
|
||||
elseif (!isset($vars['session_id']))
|
||||
// Session Id is required
|
||||
throw Exception(sprintf('session_id: %s', __('Required')));
|
||||
|
||||
if (defined('DISABLE_SESSION') && $this->data->__new__)
|
||||
return true;
|
||||
// Set User Session Atrributes
|
||||
if (!isset($vars['user_ip']))
|
||||
$vars['user_ip'] = $_SERVER['REMOTE_ADDR'];
|
||||
if (!isset($vars['user_agent']))
|
||||
$vars['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
|
||||
// Default to system SESSION_TTL if session_maxlife is not set
|
||||
$maxlife = $vars['session_maxlife'] ?? SESSION_TTL;
|
||||
// filter out the $vars to just the fields we support - backend can
|
||||
// send way more data than we want.
|
||||
$vars = array_intersect_key($vars,
|
||||
array_flip([
|
||||
'session_id',
|
||||
'session_data',
|
||||
'session_expire',
|
||||
'session_updated',
|
||||
'user_id',
|
||||
'user_ip',
|
||||
'user_agent',
|
||||
]));
|
||||
|
||||
$ttl = $this && method_exists($this, 'getTTL')
|
||||
? $this->getTTL() : SESSION_TTL;
|
||||
|
||||
// Create a session data obj if not loaded.
|
||||
if (!isset($this->data))
|
||||
$this->data = new SessionData(['session_id' => $id]);
|
||||
|
||||
$this->data->session_data = $data;
|
||||
$this->data->session_expire =
|
||||
SqlFunction::NOW()->plus(SqlInterval::SECOND($ttl));
|
||||
$this->data->user_id = $thisstaff ? $thisstaff->getId() : 0;
|
||||
$this->data->user_ip = $_SERVER['REMOTE_ADDR'];
|
||||
$this->data->user_agent = $_SERVER['HTTP_USER_AGENT'];
|
||||
|
||||
return $this->data->save();
|
||||
// Create an instance
|
||||
$record = new self($vars);
|
||||
// set session_expire timestamp based on $maxlife
|
||||
$record->setExpire($maxlife);
|
||||
// Set updated to now()
|
||||
$record->session_updated = SqlFunction::NOW();
|
||||
return $record;
|
||||
}
|
||||
|
||||
function destroy($id){
|
||||
return SessionData::objects()->filter(['session_id' => $id])->delete() ? true : false;
|
||||
static function destroy($id) {
|
||||
return ( (bool) self::objects()
|
||||
->filter(['session_id' => $id])
|
||||
->delete());
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
$this->gc(0);
|
||||
}
|
||||
|
||||
function gc($maxlife){
|
||||
SessionData::objects()->filter([
|
||||
'session_expire__lte' => SqlFunction::NOW()
|
||||
static function cleanupExpired() {
|
||||
return self::objects()->filter([
|
||||
'session_expire__lte' => SqlFunction::NOW()
|
||||
])->delete();
|
||||
}
|
||||
|
||||
static function active_sessions(array $criteria = []) {
|
||||
$now = SqlFunction::NOW();
|
||||
// Active must not be expired
|
||||
$filters = ['session_expire__gt' => $now];
|
||||
|
||||
// Authenticated users have user_id set (only Agents at the moment)
|
||||
if (isset($criteria['authenticated']) && $criteria['authenticated'])
|
||||
$filters['user_id__gt'] = 0;
|
||||
elseif (isset($criteria['authenticated']))
|
||||
$filters['user_id'] = 0; // Guests only
|
||||
|
||||
// last seen since X seconds
|
||||
if (isset($criteria['lastseen']) && $criteria['lastseen']) {
|
||||
$interval = new SqlInterval('SECOND', $criteria['lastseen']);
|
||||
$filters['session_updated__gte'] = $now->minus($interval);
|
||||
}
|
||||
|
||||
return self::objects()->filter($filters);
|
||||
}
|
||||
}
|
||||
|
||||
class MemcacheSessionBackend
|
||||
extends SessionBackend {
|
||||
var $memcache;
|
||||
var $servers = array();
|
||||
/*
|
||||
* Database Session Storage Backend
|
||||
*
|
||||
*/
|
||||
class DatabaseSessionStorageBackend
|
||||
extends osTicket\Session\AbstractSessionStorageBackend {
|
||||
|
||||
function __construct($ttl) {
|
||||
parent::__construct($ttl);
|
||||
public function expireRecord($id, $ttl) {
|
||||
if (!($record=$this->getRecord($id)))
|
||||
return false;
|
||||
|
||||
if (!extension_loaded('memcache'))
|
||||
throw new Exception('Memcached extension is missing');
|
||||
if (!defined('MEMCACHE_SERVERS'))
|
||||
return (bool) $record->expire($ttl);
|
||||
}
|
||||
|
||||
public function saveRecord($record, $secondary=false) {
|
||||
if ($secondary || !is_a($record, 'DatabaseSessionRecord')) {
|
||||
// We're only recording new sessions without data if we're
|
||||
// secondary backend
|
||||
if ($record->isNew())
|
||||
$this->writeRecord($record->getId(), null);
|
||||
return true;
|
||||
}
|
||||
return (bool) $record->commit();
|
||||
}
|
||||
|
||||
public function writeRecord($id, $data=null) {
|
||||
$record = self:: lookupRecord($id, true);
|
||||
$record->setData($data);
|
||||
return (bool) $record->commit();
|
||||
}
|
||||
|
||||
public function lookupRecord($id, $autocreate) {
|
||||
return DatabaseSessionRecord::lookupRecord($id, $autocreate, $this);
|
||||
}
|
||||
|
||||
public function destroyRecord($id) {
|
||||
return (bool) (DatabaseSessionRecord::destroy($id));
|
||||
}
|
||||
|
||||
public function cleanupExpiredRecords() {
|
||||
return DatabaseSessionRecord::cleanupExpired();
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Memchache Session Storage Backend
|
||||
*
|
||||
*/
|
||||
class MemcacheSessionStorageBackend
|
||||
extends osTicket\Session\AbstractMemcacheSessionStorageBackend {
|
||||
|
||||
public function __construct($options) {
|
||||
// Make sure we have memcache servers
|
||||
$servers = [];
|
||||
if (isset($options['servers']) && is_array($options['servers']))
|
||||
$servers = $options['servers'];
|
||||
elseif (defined('MEMCACHE_SERVERS'))
|
||||
$servers = explode(',', MEMCACHE_SERVERS);
|
||||
// Bro Got Servers or Nah?!
|
||||
if (!count($servers))
|
||||
throw new Exception('MEMCACHE_SERVERS must be defined');
|
||||
|
||||
$servers = explode(',', MEMCACHE_SERVERS);
|
||||
$this->memcache = new Memcache();
|
||||
|
||||
foreach ($servers as $S) {
|
||||
@list($host, $port) = explode(':', $S);
|
||||
if (strpos($host, '/') !== false)
|
||||
// Use port '0' for unix sockets
|
||||
$port = 0;
|
||||
else
|
||||
$port = $port ?: ini_get('memcache.default_port') ?: 11211;
|
||||
$this->servers[] = array(trim($host), (int) trim($port));
|
||||
// FIXME: Crash or warn if invalid $host or $port
|
||||
}
|
||||
$options['servers'] = $servers;
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
function getKey($id) {
|
||||
return sha1($id.SECRET_SALT);
|
||||
public function saveRecord($record, $secondary = false) {
|
||||
if ($secondary || !is_a($record, 'MemcacheSessionRecord')) {
|
||||
if (!$record->isNew())
|
||||
return true;
|
||||
}
|
||||
return $this->writeRecord($record->getId(), $record->getData());
|
||||
}
|
||||
|
||||
function read($id) {
|
||||
$key = $this->getKey($id);
|
||||
|
||||
// Try distributed read first
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->addServer($host, $port);
|
||||
}
|
||||
$data = $this->memcache->get($key);
|
||||
|
||||
// Read from other servers on failure
|
||||
if ($data === false && count($this->servers) > 1) {
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
if ($data = $this->memcache->get($key))
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// No session data on record -- new session
|
||||
$this->isnew = $data === false;
|
||||
|
||||
return $data ?: '';
|
||||
public function writeRecord($id, $data) {
|
||||
return $this->set($id, $data);
|
||||
}
|
||||
|
||||
function update($id, $data) {
|
||||
if (defined('DISABLE_SESSION') && $this->isnew)
|
||||
return true;
|
||||
|
||||
$key = $this->getKey($id);
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
if (!$this->memcache->replace($key, $data, 0, $this->getTTL()));
|
||||
$this->memcache->set($key, $data, 0, $this->getTTL());
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
function destroy($id) {
|
||||
$key = $this->getKey($id);
|
||||
foreach ($this->servers as $S) {
|
||||
list($host, $port) = $S;
|
||||
$this->memcache->pconnect($host, $port);
|
||||
$this->memcache->replace($key, '', 0, 1);
|
||||
$this->memcache->delete($key, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function gc($maxlife) {
|
||||
// Memcache does this automatically
|
||||
public function lookupRecord($id, $autocreate = false) {
|
||||
if (!($data = $this->get($id)) && !$autocreate)
|
||||
return false;
|
||||
return new MemcacheSessionRecord([
|
||||
'id' => $id,
|
||||
'new' => $data === false,
|
||||
'data' => $data,
|
||||
], $this);
|
||||
}
|
||||
}
|
||||
|
||||
class NoopSessionBackend extends SessionBackend {
|
||||
public function read($id) {
|
||||
return "";
|
||||
class MemcacheSessionRecord
|
||||
implements osTicket\Session\SessionRecordInterface {
|
||||
private $backend;
|
||||
private $data;
|
||||
|
||||
public function __construct(array $ht, $backend) {
|
||||
// Make sure we have an id
|
||||
if (!isset($ht['id']) || !$ht['id'])
|
||||
throw new Exception('Session id required');
|
||||
// Set backend
|
||||
$this->backend = $backend;
|
||||
// Set the data as array object
|
||||
$this->data = new ArrayObject($ht, ArrayObject::ARRAY_AS_PROPS);
|
||||
}
|
||||
|
||||
public function update($id, $data) {
|
||||
public function isNew() {
|
||||
return ($this->data->new);
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
return (strlen($this->getId()) == 32);
|
||||
}
|
||||
|
||||
public function getId() {
|
||||
return $this->data->id;
|
||||
}
|
||||
|
||||
public function setId(string $id) {
|
||||
$this->data->id = $id;
|
||||
}
|
||||
|
||||
public function getData() {
|
||||
return $this->data->data;
|
||||
}
|
||||
|
||||
public function setData(string $data = null) {
|
||||
$this->data->data = $data;
|
||||
}
|
||||
|
||||
public function setTTl(int $ttl) {
|
||||
$this->data->ttl = $ttl;
|
||||
}
|
||||
|
||||
// Unsupported call but required
|
||||
public function expire(int $ttl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy($id) {
|
||||
return true;
|
||||
public function commit() {
|
||||
return $this->backend
|
||||
? $this->backend->saveRecord($this)
|
||||
: false;
|
||||
}
|
||||
|
||||
public function gc($maxlife) {
|
||||
return true;
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'data' => $this->getData(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function create($vars) {
|
||||
return new self($vars);
|
||||
}
|
||||
}
|
||||
|
||||
class FallbackSessionBackend {
|
||||
// Use default PHP settings, with some edits for best experience
|
||||
function __construct() {
|
||||
// FIXME: Consider extra possible security tweaks such as adjusting
|
||||
// the session.save_path
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
/*********************************************************************
|
||||
class.usersession.php
|
||||
|
||||
User (client and staff) sessions handle.
|
||||
User (client and staff) sessions manager
|
||||
|
||||
User-Space session management, not to confused with Session Storage
|
||||
Backends.
|
||||
|
||||
Peter Rotich <peter@osticket.com>
|
||||
Copyright (c) 2006-2013 osTicket
|
||||
Copyright (c) 2022 osTicket
|
||||
http://www.osticket.com
|
||||
|
||||
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
||||
@@ -52,9 +55,12 @@ class UserSession {
|
||||
}
|
||||
|
||||
function sessionToken(){
|
||||
// Please note that user-space token is not meant to be secure at all
|
||||
// we're simply encoding stuff we want to track as we refresh the
|
||||
// session.
|
||||
$time = time();
|
||||
$hash = md5($time.SESSION_SECRET.$this->userID);
|
||||
$token = "$hash:$time:".MD5($this->ip);
|
||||
$token = "$hash:$time:".MD5($this->getIP());
|
||||
return $token;
|
||||
}
|
||||
|
||||
@@ -66,35 +72,31 @@ class UserSession {
|
||||
return $expire;
|
||||
}
|
||||
|
||||
function isvalidSession($htoken,$maxidletime=0,$checkip=false){
|
||||
global $cfgi;
|
||||
function isvalidSession($htoken, $maxidletime=0, $checkip=false){
|
||||
global $cfg;
|
||||
|
||||
// Compare session ids
|
||||
if (strcmp($this->getSessionId(), session_id()))
|
||||
return false;
|
||||
|
||||
// Is the session invalidated?
|
||||
if (isset($_SESSION['KAPUT']) && $_SESSION['KAPUT'] < time())
|
||||
return (session_destroy() && false);
|
||||
|
||||
$token = rawurldecode($htoken);
|
||||
// Check if we got what we expected....
|
||||
if ($token && !strstr($token,":"))
|
||||
return false;
|
||||
|
||||
// Get the goodies
|
||||
list($hash, $expire, $ip) = explode(":",$token);
|
||||
list($hash, $expire, $ip) = explode(':', $token);
|
||||
|
||||
// Make sure the session hash is valid
|
||||
if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
|
||||
return false;
|
||||
|
||||
// is it expired??
|
||||
if ($maxidletime && ((time()-$expire)>$maxidletime))
|
||||
if ($maxidletime && ((time()-$expire) > $maxidletime))
|
||||
return false;
|
||||
|
||||
#Make sure IP is still same ( proxy access??????)
|
||||
if ($checkip && strcmp($ip, MD5($this->ip)))
|
||||
// Make sure IP is still same - if requested
|
||||
if ($checkip && strcmp($ip, MD5($this->getIP())))
|
||||
return false;
|
||||
|
||||
$this->validated = true;
|
||||
@@ -102,54 +104,13 @@ class UserSession {
|
||||
return true;
|
||||
}
|
||||
|
||||
function regenerateSession($destroy=false) {
|
||||
// Delayed kaput time for current session
|
||||
$_SESSION['KAPUT'] = time() + 60;
|
||||
// Save the session id as old
|
||||
$old = session_id();
|
||||
// Regenerate the session without destroying data
|
||||
session_regenerate_id(false);
|
||||
// Get new session id and close
|
||||
$new = session_id();
|
||||
session_write_close();
|
||||
// Start new session
|
||||
session_id($new);
|
||||
session_start();
|
||||
$this->session_id = $new;
|
||||
// Make sure new session is not set to KAPUT and TIME_BOMB
|
||||
unset($_SESSION['KAPUT'], $_SESSION['TIME_BOMB']);
|
||||
// Destroy ?
|
||||
if ($destroy) {
|
||||
// Destrory old session
|
||||
$this->destroySession($old);
|
||||
// Restore new session
|
||||
session_id($new);
|
||||
session_start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function destroySession($id) {
|
||||
// Close current session
|
||||
session_write_close();
|
||||
// Start target session
|
||||
session_id($id);
|
||||
session_start();
|
||||
// Destroy session
|
||||
session_destroy();
|
||||
session_write_close();
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValid() {
|
||||
return ($this->validated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
trait UserSessionTrait {
|
||||
// Session Object
|
||||
// User Session Object
|
||||
var $session;
|
||||
// Session Token
|
||||
var $token;
|
||||
@@ -160,23 +121,55 @@ trait UserSessionTrait {
|
||||
// User class
|
||||
var $class = '';
|
||||
|
||||
function refreshSession($refreshRate=60): void {
|
||||
// If TIME_BOMB isset and less than the current time we need to regenerate
|
||||
// session to help mitigate session fixation
|
||||
if (isset($_SESSION['TIME_BOMB']) && ($_SESSION['TIME_BOMB'] < time()))
|
||||
$this->regenerateSession();
|
||||
|
||||
public function getMaxIdleTime() {
|
||||
return $this->maxidletime ;
|
||||
}
|
||||
|
||||
function refreshSession($refreshRate=60) {
|
||||
// Check Time To Die (TTD) if any - OLD people.. I mean sessions,
|
||||
// must die! Don't fight it bro!
|
||||
if (isset($_SESSION['TTD']) && $_SESSION['TTD'] < time()) {
|
||||
error_log(sprintf('Session %s with TTD %s was used',
|
||||
session_id(), $_SESSION['TTD']));
|
||||
return (session_destroy() && false);
|
||||
}
|
||||
|
||||
// If TIME_BOMB is set and less than the current time we need to regenerate
|
||||
// session id to help mitigate session fixation attacks.
|
||||
if (isset($_SESSION['TIME_BOMB'])
|
||||
&& ($_SESSION['TIME_BOMB'] < time())
|
||||
&& ($id=$this->regenerateSession())) {
|
||||
// unset timer and et next one 24 hrs later
|
||||
// TODO: Make regenerate frequency configurable in 2032 /j
|
||||
// PS: Living and dying and the stories that are true Secrets to
|
||||
// a good life is knowing when you're through ~ time bomb
|
||||
$_SESSION['TIME_BOMB'] = time() + 86400;
|
||||
// set id
|
||||
$this->session_id = $id;
|
||||
// Force cookie renewal NOW!
|
||||
$refreshRate = -1;
|
||||
}
|
||||
|
||||
// Deadband session token updates to once / 30-seconds
|
||||
$updated = $this->session->getLastUpdate($this->token);
|
||||
if ($updated + $refreshRate < time()) {
|
||||
// Renew the session token
|
||||
$this->token = $this->getSessionToken();
|
||||
osTicketSession::renewCookie($updated, $this->maxidletime);
|
||||
// Update the expire time for the session cookie
|
||||
osTicketSession::renewCookie(time(), $this->getMaxIdleTime());
|
||||
}
|
||||
}
|
||||
|
||||
function regenerateSession($destroy=false) {
|
||||
$this->session->regenerateSession($destroy);
|
||||
// Set cookie for the new session id.
|
||||
$this->refreshSession(-1);
|
||||
function regenerateSession(int $ttl = 120) {
|
||||
// Set TTD (Time To Die) on current session
|
||||
// If ttl is 0 then session is destroyed immediatetly
|
||||
$_SESSION['TTD'] = time() + $ttl; // now + ttl
|
||||
if (($id=osTicketSession::regenerate($ttl)))
|
||||
$this->session_id = $id;
|
||||
// unset TTD on the new session - new life my boy!
|
||||
unset($_SESSION['TTD']);
|
||||
return $id;
|
||||
}
|
||||
|
||||
function getSession() {
|
||||
@@ -202,7 +195,7 @@ trait UserSessionTrait {
|
||||
function isValidSession() {
|
||||
return ($this->getId()
|
||||
&& $this->session->isvalidSession($this->token,
|
||||
$this->maxidletime, $this->checkip));
|
||||
$this->getMaxIdleTime(), $this->checkip));
|
||||
}
|
||||
|
||||
abstract function isValid();
|
||||
|
||||
5
logo.php
5
logo.php
@@ -15,9 +15,8 @@
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
// Disable updating session data - false still starts the session but data
|
||||
// write is ignored.
|
||||
define('DISABLE_SESSION', false);
|
||||
// Use Noop Session Handler
|
||||
define('NOOP_SESSION', true);
|
||||
require('client.inc.php');
|
||||
$ttl = 86400; // max-age
|
||||
if (($logo = $ost->getConfig()->getClientLogo())) {
|
||||
|
||||
@@ -20,6 +20,6 @@ if ($thisclient && $_GET['auth'] && $ost->validateLinkToken($_GET['auth']))
|
||||
$thisclient->logOut();
|
||||
|
||||
osTicketSession::destroyCookie();
|
||||
|
||||
session_destroy();
|
||||
Http::redirect('index.php');
|
||||
?>
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
|
||||
vim: expandtab sw=4 ts=4 sts=4:
|
||||
**********************************************************************/
|
||||
// Disable updating session data - false still starts the session but data
|
||||
// write is ignored.
|
||||
define('DISABLE_SESSION', false);
|
||||
// Use Noop Session Handler
|
||||
define('NOOP_SESSION', true);
|
||||
require_once('../main.inc.php');
|
||||
$ttl = 86400; // max-age
|
||||
if (isset($_GET['backdrop'])) {
|
||||
|
||||
@@ -22,15 +22,9 @@ if(!$_GET['auth'] || !$ost->validateLinkToken($_GET['auth']))
|
||||
|
||||
try {
|
||||
$thisstaff->logOut();
|
||||
|
||||
//Destroy session on logout.
|
||||
// TODO: Stop doing this starting with 1.9 - separate session data per
|
||||
// app/panel.
|
||||
session_unset();
|
||||
session_destroy();
|
||||
|
||||
osTicketSession::destroyCookie();
|
||||
|
||||
session_destroy();
|
||||
//Clear any ticket locks the staff has.
|
||||
Lock::removeStaffLocks($thisstaff->getId());
|
||||
}
|
||||
|
||||
@@ -96,6 +96,9 @@ if(!$thisstaff->isAdmin()) {
|
||||
exit;
|
||||
}
|
||||
}
|
||||
/******* SET STAFF DEFAULTS **********/
|
||||
define('PAGE_LIMIT', $thisstaff->getPageLimit() ?: DEFAULT_PAGE_LIMIT);
|
||||
define('SESSION_MAXLIFE', $thisstaff->getMaxIdleTime());
|
||||
|
||||
//Keep the session activity alive
|
||||
$thisstaff->refreshSession();
|
||||
@@ -113,8 +116,6 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'"
|
||||
// Load the navigation after the user in case some things are hidden
|
||||
require_once(INCLUDE_DIR.'class.nav.php');
|
||||
|
||||
/******* SET STAFF DEFAULTS **********/
|
||||
define('PAGE_LIMIT', $thisstaff->getPageLimit() ?: DEFAULT_PAGE_LIMIT);
|
||||
|
||||
$tabs=array();
|
||||
$submenu=array();
|
||||
|
||||
Reference in New Issue
Block a user