From 5a05fde9f087d9005f980268b7b66d8467631449 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 19 Apr 2014 00:01:33 +0200 Subject: [PATCH] Added support for virtual sessions Virtual Sessions extend the original session system with a transparent layer. It's only purpose is to enforce session validation based on IP address and/or user agent. The legacy session system does not allow the same user being logged-in more than once and the same is true for WCF 2.1 unless we break most parts of the API. In order to solve this, we do allow multiple clients to share the exact same session among them, while the individual clients are tracked within wcf1_session_virtual. --- .../lib/data/acp/session/ACPSession.class.php | 10 ++ .../files/lib/data/session/Session.class.php | 30 ++++ .../session/virtual/SessionVirtual.class.php | 66 +++++++ .../virtual/SessionVirtualAction.class.php | 38 ++++ .../virtual/SessionVirtualEditor.class.php | 29 +++ .../virtual/SessionVirtualList.class.php | 20 +++ .../system/session/SessionHandler.class.php | 166 ++++++++++++++---- wcfsetup/setup/db/install.sql | 12 ++ 8 files changed, 339 insertions(+), 32 deletions(-) create mode 100644 wcfsetup/install/files/lib/data/session/virtual/SessionVirtual.class.php create mode 100644 wcfsetup/install/files/lib/data/session/virtual/SessionVirtualAction.class.php create mode 100644 wcfsetup/install/files/lib/data/session/virtual/SessionVirtualEditor.class.php create mode 100644 wcfsetup/install/files/lib/data/session/virtual/SessionVirtualList.class.php diff --git a/wcfsetup/install/files/lib/data/acp/session/ACPSession.class.php b/wcfsetup/install/files/lib/data/acp/session/ACPSession.class.php index d786b1bdc1..e8d8b5c693 100644 --- a/wcfsetup/install/files/lib/data/acp/session/ACPSession.class.php +++ b/wcfsetup/install/files/lib/data/acp/session/ACPSession.class.php @@ -36,4 +36,14 @@ class ACPSession extends DatabaseObject { public static function supportsPersistentLogins() { return false; } + + /** + * Returns true if this session type supports virtual sessions (sharing the same + * session among multiple clients). + * + * @return boolean + */ + public static function supportsVirtualSessions() { + return false; + } } diff --git a/wcfsetup/install/files/lib/data/session/Session.class.php b/wcfsetup/install/files/lib/data/session/Session.class.php index dae97041a5..ca548e49e2 100644 --- a/wcfsetup/install/files/lib/data/session/Session.class.php +++ b/wcfsetup/install/files/lib/data/session/Session.class.php @@ -1,6 +1,7 @@ prepareStatement($sql); + $statement->execute(array($userID)); + $row = $statement->fetchArray(); + + if ($row === false) { + return null; + } + + return new static(null, $row); + } } diff --git a/wcfsetup/install/files/lib/data/session/virtual/SessionVirtual.class.php b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtual.class.php new file mode 100644 index 0000000000..2c2dc38b33 --- /dev/null +++ b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtual.class.php @@ -0,0 +1,66 @@ + + * @package com.woltlab.wcf + * @subpackage data.session.virtual + * @category Community Framework + */ +class SessionVirtual extends DatabaseObject { + /** + * @see \wcf\data\DatabaseObject::$databaseTableName + */ + protected static $databaseTableName = 'session_virtual'; + + /** + * @see \wcf\data\DatabaseObject::$databaseTableIndexName + */ + protected static $databaseTableIndexName = 'virtualSessionID'; + + /** + * Returns the active virtual session object or null. + * + * @param string $sessionID + * @return \wcf\data\session\virtual\SessionVirtual + */ + public static function getExistingSession($sessionID) { + $sql = "SELECT * + FROM ".static::getDatabaseTableName()." + WHERE sessionID = ? + AND ipAddress = ? + AND userAGent = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $sessionID, + UserUtil::getIpAddress(), + UserUtil::getUserAgent() + )); + + return $statement->fetchObject(__CLASS__); + } + + /** + * Returns the number of virtual sessions associated with the given session id. + * + * @param string $sessionID + * @return integer + */ + public static function countVirtualSessions($sessionID) { + $sql = "SELECT COUNT(*) AS count + FROM ".static::getDatabaseTableName()." + WHERE sessionID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array($sessionID)); + $row = $statement->fetchArray(); + + return $row['count']; + } +} diff --git a/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualAction.class.php b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualAction.class.php new file mode 100644 index 0000000000..577236a5d8 --- /dev/null +++ b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualAction.class.php @@ -0,0 +1,38 @@ + + * @package com.woltlab.wcf + * @subpackage data.session.virtual + * @category Community Framework + */ +class SessionVirtualAction extends AbstractDatabaseObjectAction { + /** + * @see \wcf\data\AbstractDatabaseObjectAction::$className + */ + protected $className = 'wcf\data\session\virtual\SessionVirtualEditor'; + + /** + * Attention: This method does not always return a new object, in case a matching virtual session + * already exists, the existing session will be returned rather than a new session being created. + * + * @see \wcf\data\AbstractDatabaseObjectAction::create() + */ + public function create() { + // try to find an existing virtual session + $virtualSession = call_user_func(array($this->className, 'getExistingSession'), $this->parameters['sessionID']); + if ($virtualSession !== null) { + return $virtualSession; + } + + if (!isset($this->parameters['lastActivityTime'])) $this->parameters['lastActivityTime'] = TIME_NOW; + + return parent::create(); + } +} diff --git a/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualEditor.class.php b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualEditor.class.php new file mode 100644 index 0000000000..4c9e909835 --- /dev/null +++ b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualEditor.class.php @@ -0,0 +1,29 @@ + + * @package com.woltlab.wcf + * @subpackage data.session.virtual + * @category Community Framework + */ +class SessionVirtualEditor extends DatabaseObjectEditor { + /** + * @see \wcf\data\DatabaseObjectDecorator::$baseClass + */ + protected static $baseClass = 'wcf\data\session\virtual\SessionVirtual'; + + /** + * Updates last activity time of this virtual session. + */ + public function updateLastActivityTime() { + $this->update(array( + 'lastActivityTime' => TIME_NOW + )); + } +} diff --git a/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualList.class.php b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualList.class.php new file mode 100644 index 0000000000..38b70b03c1 --- /dev/null +++ b/wcfsetup/install/files/lib/data/session/virtual/SessionVirtualList.class.php @@ -0,0 +1,20 @@ + + * @package com.woltlab.wcf + * @subpackage data.session.virtual + * @category Community Framework + */ +class SessionVirtualList extends DatabaseObjectList { + /** + * @see \wcf\data\DatabaseObjectList::$className + */ + public $className = 'wcf\data\session\virtual\SessionVirtual'; +} diff --git a/wcfsetup/install/files/lib/system/session/SessionHandler.class.php b/wcfsetup/install/files/lib/system/session/SessionHandler.class.php index c9bcc24915..d15375e3ee 100644 --- a/wcfsetup/install/files/lib/system/session/SessionHandler.class.php +++ b/wcfsetup/install/files/lib/system/session/SessionHandler.class.php @@ -1,5 +1,8 @@ supportsVirtualSessions = call_user_func(array($this->sessionClassName, 'supportsVirtualSessions')); + } + /** * Provides access to session data. * @@ -297,13 +319,38 @@ class SessionHandler extends SingletonFactory { */ protected function getExistingSession($sessionID) { $this->session = new $this->sessionClassName($sessionID); - if (!$this->session->sessionID || !$this->validate()) { + if (!$this->session->sessionID) { $this->session = null; return; } - // load user $this->user = new User($this->session->userID); + $this->loadVirtualSession(); + + if (!$this->validate()) { + $this->session = null; + $this->user = null; + $this->virtualSession = false; + + return; + } + } + + /** + * Loads the virtual session object unless the user is not logged in or the session + * does not support virtual sessions. If there is no virtual session yet, it will be + * created on-the-fly. + * + * @param boolean $forceReload + */ + protected function loadVirtualSession($forceReload = false) { + if ($this->virtualSession === false || $forceReload) { + $this->virtualSession = null; + if ($this->user->userID && $this->supportsVirtualSessions) { + $virtualSessionAction = new SessionVirtualAction(array(), 'create', array('sessionID' => $this->session->sessionID)); + $this->virtualSession = $virtualSessionAction->executeAction(); + } + } } /** @@ -313,12 +360,23 @@ class SessionHandler extends SingletonFactory { */ protected function validate() { if (SESSION_VALIDATE_IP_ADDRESS) { - if ($this->session->ipAddress != UserUtil::getIpAddress()) { + if ($this->supportsVirtualSessions && ($this->virtualSession instanceof SessionVirtual)) { + if ($this->virtualSession->ipAddress != UserUtil::getIpAddress()) { + return false; + } + } + else if ($this->session->ipAddress != UserUtil::getIpAddress()) { return false; } } + if (SESSION_VALIDATE_USER_AGENT) { - if ($this->session->userAgent != UserUtil::getUserAgent()) { + if ($this->supportsVirtualSessions && ($this->virtualSession instanceof SessionVirtual)) { + if ($this->virtualSession->userAgent != UserUtil::getUserAgent()) { + return false; + } + } + else if ($this->session->userAgent != UserUtil::getUserAgent()) { return false; } } @@ -356,9 +414,7 @@ class SessionHandler extends SingletonFactory { // create guest user $this->user = new User(null); } - - if ($this->user->userID != 0) { - // user is no guest + else if (!$this->supportsVirtualSessions) { // delete all other sessions of this user call_user_func(array($this->sessionEditorClassName, 'deleteUserSessions'), array($this->user->userID)); } @@ -373,8 +429,10 @@ class SessionHandler extends SingletonFactory { 'requestURI' => UserUtil::getRequestURI(), 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ? substr($_SERVER['REQUEST_METHOD'], 0, 7) : '') ); + if ($spiderID !== null) $sessionData['spiderID'] = $spiderID; $this->session = call_user_func(array($this->sessionEditorClassName, 'create'), $sessionData); + $this->loadVirtualSession(); } /** @@ -480,28 +538,50 @@ class SessionHandler extends SingletonFactory { public function changeUser(User $user, $hideSession = false) { $sessionTable = call_user_func(array($this->sessionClassName, 'getDatabaseTableName')); - if ($user->userID && !$hideSession) { - // user is not a guest, delete all other sessions of this user - $sql = "DELETE FROM ".$sessionTable." - WHERE sessionID <> ? - AND userID = ?"; - $statement = WCF::getDB()->prepareStatement($sql); - $statement->execute(array($this->sessionID, $user->userID)); - - // reset session variables - $this->variables = array(); - $this->variablesChanged = true; + $isNewSession = true; + if ($user->userID) { + if ($this->supportsVirtualSessions) { + // find existing session + $session = call_user_func(array($this->sessionClassName, 'getSessionByUserID'), $user->userID); + + if ($session !== null) { + // delete guest session + $sessionEditor = new $this->sessionEditorClassName($this->session); + $sessionEditor->delete(); + + // inherit existing session + $this->session = $session; + $this->user = $user; + $this->loadVirtualSession(true); + + $isNewSession = false; + } + } + else if (!$hideSession) { + // user is not a guest, delete all other sessions of this user + $sql = "DELETE FROM ".$sessionTable." + WHERE sessionID <> ? + AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array($this->sessionID, $user->userID)); + + // reset session variables + $this->variables = array(); + $this->variablesChanged = true; + } } - // update user reference - $this->user = $user; - - if (!$hideSession) { - // update session - $sessionEditor = new $this->sessionEditorClassName($this->session); - $sessionEditor->update(array( - 'userID' => $this->user->userID - )); + if ($isNewSession) { + // update user reference + $this->user = $user; + + if (!$hideSession) { + // update session + $sessionEditor = new $this->sessionEditorClassName($this->session); + $sessionEditor->update(array( + 'userID' => $this->user->userID + )); + } } // reset caches @@ -509,8 +589,6 @@ class SessionHandler extends SingletonFactory { $this->languageIDs = null; $this->languageID = $this->user->languageID; $this->styleID = $this->user->styleID; - - // truncate session variables } /** @@ -541,6 +619,11 @@ class SessionHandler extends SingletonFactory { // update session $sessionEditor = new $this->sessionEditorClassName($this->session); $sessionEditor->update($data); + + if ($this->virtualSession instanceof SessionVirtual) { + $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession); + $virtualSessionEditor->updateLastActivityTime(); + } } /** @@ -554,6 +637,11 @@ class SessionHandler extends SingletonFactory { $sessionEditor->update(array( 'lastActivityTime' => TIME_NOW )); + + if ($this->virtualSession instanceof SessionVirtual) { + $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession); + $virtualSessionEditor->updateLastActivityTime(); + } } /** @@ -563,7 +651,7 @@ class SessionHandler extends SingletonFactory { // clear storage if ($this->user->userID) { self::resetSessions(array($this->user->userID)); - + // update last activity time if (!class_exists('\wcf\system\WCFACP', false)) { $editor = new UserEditor($this->user); @@ -575,8 +663,22 @@ class SessionHandler extends SingletonFactory { $this->changeUser(new User(null), true); // remove session - $sessionEditor = new $this->sessionEditorClassName($this->session); - $sessionEditor->delete(); + $deleteSession = true; + if ($this->supportsVirtualSessions && ($this->virtualSession instanceof SessionVirtual)) { + // delete the virtual session + $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession); + $virtualSessionEditor->delete(); + + if (SessionVirtual::countVirtualSessions($this->session->sessionID)) { + // there are still remaining virtual sessions, do not delete master session + $deleteSession = false; + } + } + + if ($deleteSession) { + $sessionEditor = new $this->sessionEditorClassName($this->session); + $sessionEditor->delete(); + } // disable update $this->disableUpdate(); diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index cb27703495..b47f1cd7c6 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -812,6 +812,16 @@ CREATE TABLE wcf1_session ( KEY packageID (lastActivityTime, spiderID) ); +DROP TABLE IF EXISTS wcf1_session_virtual; +CREATE TABLE wcf1_session_virtual ( + virtualSessionID INT(10) NOT NULL PRIMARY KEY, + sessionID CHAR(40) NOT NULL, + ipAddress VARCHAR(39) NOT NULL DEFAULT '', + userAgent VARCHAR(255) NOT NULL DEFAULT '', + lastActivityTime INT(10) NOT NULL DEFAULT 0, + UNIQUE KEY (sessionID, ipAddress, userAgent) +); + DROP TABLE IF EXISTS wcf1_sitemap; CREATE TABLE wcf1_sitemap ( sitemapID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -1407,6 +1417,8 @@ ALTER TABLE wcf1_search ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) O ALTER TABLE wcf1_session ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; ALTER TABLE wcf1_session ADD FOREIGN KEY (spiderID) REFERENCES wcf1_spider (spiderID) ON DELETE CASCADE; +ALTER TABLE wcf1_session_virtual ADD FOREIGN KEY (sessionID) REFERENCES wcf1_session (sessionID) ON DELETE CASCADE; + ALTER TABLE wcf1_sitemap ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; ALTER TABLE wcf1_smiley ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; -- 2.20.1