Repository: evennia/evennia

Issue:
[BUG] Inconsistent session tracking between websocket and AJAX webclient
#### Describe the bug
When connecting to evennia with websockets, each tab establishes its own websocket and counts as a separate session for the server.

However, by default, a browser uses the same _website_ session across all tabs. The AJAX method references this same one session for all requests, so all tabs count as the _same_ session for the server and will override each other, even in multisession modes.

#### Additional context
I don't think either one of these is necessarily wrong or correct, but the fact that they behave differently is less than ideal. That said, I think it would probably be simpler to make the websocket behave like the AJAX method than vice versa.


Discussion:
I studied this issue for #3335 and here are my thoughts:

The Django session middleware expects each 'session' to be its own context.  The AJAX webclient absolutely should open a NEW session with the same login, and save it for the duration of that window. Don't cookie it. Use it only for that window.  That way each AJAX window will be 100% independent.

You may be able to apply the same logic to browser windows as well, actually.  But whether that's the correct behavior is up for debate.
The planned move to `SessionDB` will involve a refurbishment of the session system down the line. In the interim though, yeah, probably best to go with the websocket isolation-of-session approach here, however one reproduces that with the ajax client. 

Resolving patch:
diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py
--- a/evennia/server/portal/webclient.py
+++ b/evennia/server/portal/webclient.py
@@ -68,7 +68,7 @@ def get_client_session(self):
 
         """
         try:
-            # client will connect with wsurl?csessid&browserid
+            # client will connect with wsurl?csessid&page_id&browserid
             webarg = self.http_request_uri.split("?", 1)[1]
         except IndexError:
             # this may happen for custom webclients not caring for the
@@ -82,9 +82,12 @@ def get_client_session(self):
             logger.log_trace(str(self))
             return None
 
-        self.csessid, *browserstr = webarg.split("&", 1)
-        if browserstr:
-            self.browserstr = str(browserstr[0])
+        self.csessid, *cargs = webarg.split("&", 2)
+        if len(cargs) == 1:
+            self.browserstr = str(cargs[0])
+        elif len(cargs) == 2:
+            self.page_id = str(cargs[0])
+            self.browserstr = str(cargs[1])
 
         if self.csessid:
             return _CLIENT_SESSIONS(session_key=self.csessid)
diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py
--- a/evennia/server/portal/webclient_ajax.py
+++ b/evennia/server/portal/webclient_ajax.py
@@ -270,6 +270,18 @@ def get_client_sessid(self, request):
         """
         return html.escape(request.args[b"csessid"][0].decode("utf-8"))
 
+    def get_client_page_id(self, request):
+        """
+        Helper to get the client page id out of the request.
+
+        Args:
+            request (Request): Incoming request object.
+        Returns:
+            csessid (int): The client-page id.
+
+        """
+        return html.escape(request.args[b"cuid"][0].decode("utf-8"))
+
     def get_browserstr(self, request):
         """
         Get browser-string out of the request.
@@ -313,10 +325,10 @@ def lineSend(self, csessid, data):
 
     def client_disconnect(self, csessid):
         """
-        Disconnect session with given csessid.
+        Disconnect session with given id.
 
         Args:
-            csessid (int): Session id.
+            csessid (int): Client page+session id.
 
         """
         if csessid in self.requests:
@@ -334,7 +346,8 @@ def mode_init(self, request):
             request (Request): Incoming request.
 
         """
-        csessid = self.get_client_sessid(request)
+        session_id = self.get_client_sessid(request)
+        page_id = self.get_client_page_id(request)
         browserstr = self.get_browserstr(request)
 
         remote_addr = ip_from_request(request)
@@ -349,9 +362,9 @@ def mode_init(self, request):
         sess.client = self
         sess.init_session("ajax/comet", remote_addr, self.sessionhandler)
 
-        sess.csessid = csessid
+        sess.csessid = session_id+page_id
         sess.browserstr = browserstr
-        csession = _CLIENT_SESSIONS(session_key=sess.csessid)
+        csession = _CLIENT_SESSIONS(session_key=session_id)
         uid = csession and csession.get("webclient_authenticated_uid", False)
         if uid:
             # the client session is already logged in
@@ -359,7 +372,7 @@ def mode_init(self, request):
             sess.logged_in = True
 
         # watch for dead links
-        self.last_alive[csessid] = (time.time(), False)
+        self.last_alive[sess.csessid] = (time.time(), False)
         if not self.keep_alive:
             # the keepalive is not running; start it.
             self.keep_alive = LoopingCall(self._keepalive)
@@ -373,7 +386,7 @@ def mode_init(self, request):
         # actually do the connection
         sess.sessionhandler.connect(sess)
 
-        return jsonify({"msg": host_string, "csessid": csessid})
+        return jsonify({"msg": host_string, "csessid": session_id})
 
     def mode_keepalive(self, request):
         """
@@ -384,7 +397,7 @@ def mode_keepalive(self, request):
             request (Request): Incoming request.
 
         """
-        csessid = self.get_client_sessid(request)
+        csessid = self.get_client_sessid(request) + self.get_client_page_id(request)
         self.last_alive[csessid] = (time.time(), False)
         return b'""'
 
@@ -397,7 +410,7 @@ def mode_input(self, request):
             request (Request): Incoming request.
 
         """
-        csessid = self.get_client_sessid(request)
+        csessid = self.get_client_sessid(request) + self.get_client_page_id(request)
         self.last_alive[csessid] = (time.time(), False)
         cmdarray = json.loads(request.args.get(b"data")[0])
         for sess in self.sessionhandler.sessions_from_csessid(csessid):
@@ -415,7 +428,7 @@ def mode_receive(self, request):
             request (Request): Incoming request.
 
         """
-        csessid = html.escape(request.args[b"csessid"][0].decode("utf-8"))
+        csessid = self.get_client_sessid(request) + self.get_client_page_id(request)
         self.last_alive[csessid] = (time.time(), False)
 
         dataentries = self.databuffer.get(csessid)
@@ -441,7 +454,7 @@ def mode_close(self, request):
             request (Request): Incoming request.
 
         """
-        csessid = self.get_client_sessid(request)
+        csessid = self.get_client_sessid(request) + self.get_client_page_id(request)
         try:
             sess = self.sessionhandler.sessions_from_csessid(csessid)[0]
             sess.sessionhandler.disconnect(sess)


Test patch: