diff --git a/evennia/server/inputfuncs.py b/evennia/server/inputfuncs.py index 5853bf10e7..086890562c 100644 --- a/evennia/server/inputfuncs.py +++ b/evennia/server/inputfuncs.py @@ -27,7 +27,6 @@ from evennia.players.models import PlayerDB from evennia.utils.logger import log_err from evennia.utils.utils import to_str, to_unicode -# django browser sessions BrowserSessionStore = importlib.import_module(settings.SESSION_ENGINE).SessionStore diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 959df40deb..78e5e8a02c 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -137,7 +137,7 @@ class PortalSessionHandler(SessionHandler): sessdata = dict((key, val) for key, val in sessdata.items() if key in ("protocol_key", "address", "sessid", - "suid", + "csessid", "conn_time", "protocol_flags", "server_data",)) @@ -274,20 +274,20 @@ class PortalSessionHandler(SessionHandler): """ return len(self.get_sessions(include_unloggedin=include_unloggedin)) - def session_from_suid(self, suid): + def session_from_csessid(self, csessid): """ Given a session id, retrieve the session (this is primarily intended to be called by web clients) Args: - suid (int): Session id. + csessid (int): Session id. Returns: session (list): The matching session, if found. """ return [sess for sess in self.get_sessions(include_unloggedin=True) - if hasattr(sess, 'suid') and sess.suid == suid] + if hasattr(sess, 'csessid') and sess.csessid == csessid] def announce_all(self, message): """ diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index 6ac3171a0b..5af4270b35 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -32,11 +32,12 @@ import json from twisted.internet.protocol import Protocol from django.conf import settings from evennia.server.session import Session -from evennia.utils.utils import to_str +from evennia.utils.utils import to_str, mod_import from evennia.utils.ansi import parse_ansi from evennia.utils.text2html import parse_html _RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE) +_CLIENT_SESSIONS = mod_import(settings.SESSION_ENGINE).SessionStore class WebSocketClient(Protocol, Session): @@ -52,6 +53,8 @@ class WebSocketClient(Protocol, Session): self.transport.validationMade = self.validationMade client_address = self.transport.client client_address = client_address[0] if client_address else None + print ("connectionMade: webclient address", client_address, self.transport.client, self.transport.client.__dict__, self.transport.__dict__) + self.init_session("websocket", client_address, self.factory.sessionhandler) # watch for dead links self.transport.setTcpKeepAlive(1) @@ -123,15 +126,36 @@ class WebSocketClient(Protocol, Session): kwargs (any): Options from protocol. Notes: - The websocket client can send the - "websocket_close" command to report - that the client has been closed and - that the session should be disconnected. + At initilization, the client will send the special + 'csessid' command to identify its browser session hash + with the Evennia side. + + The websocket client will also pass 'websocket_close' command + to report that the client has been closed and that the + session should be disconnected. + + Both those commands are parsed and extracted already at + this point. """ + + if "csessid" in kwargs and self.csessid is None: + # only allow to change csessid on the very first connect + # - this is a safety measure to avoid a clself.transport.client.__dict__, ient to manually + # change its csessid later. + self.csessid = kwargs.pop("csessid") + csession = _CLIENT_SESSIONS(session_key=self.csessid) + uid = csession and csession.get("logged_in", False) + if uid: + # the browser session is already logged in. + self.uid = uid + self.logged_in = True + return + if "websocket_close" in kwargs: self.disconnect() return + self.sessionhandler.data_in(self, **kwargs) def data_out(self, **kwargs): diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py index ec795d7152..d5ec8cadaf 100644 --- a/evennia/server/portal/webclient_ajax.py +++ b/evennia/server/portal/webclient_ajax.py @@ -20,7 +20,6 @@ import json import re from time import time -from hashlib import md5 from twisted.web import server, resource from twisted.internet.task import LoopingCall from django.utils.functional import Promise @@ -71,10 +70,10 @@ class WebClient(resource.Resource): self.last_alive = {} self.keep_alive = None - def _responseFailed(self, failure, suid, request): + def _responseFailed(self, failure, csessid, request): "callback if a request is lost/timed out" try: - del self.requests[suid] + del self.requests[csessid] except KeyError: pass @@ -84,62 +83,62 @@ class WebClient(resource.Resource): """ now = time() to_remove = [] - keep_alives = ((suid, remove) for suid, (t, remove) + keep_alives = ((csessid, remove) for csessid, (t, remove) in self.last_alive.iteritems() if now - t > _KEEPALIVE) - for suid, remove in keep_alives: + for csessid, remove in keep_alives: if remove: # keepalive timeout. Line is dead. - to_remove.append(suid) + to_remove.append(csessid) else: # normal timeout - send keepalive - self.last_alive[suid] = (now, True) - self.lineSend(suid, ["ajax_keepalive", [], {}]) + self.last_alive[csessid] = (now, True) + self.lineSend(csessid, ["ajax_keepalive", [], {}]) # remove timed-out sessions - for suid in to_remove: - sess = self.sessionhandler.session_from_suid(suid) + for csessid in to_remove: + sess = self.sessionhandler.sessions_from_csessid(csessid) if sess: sess[0].disconnect() - self.last_alive.pop(suid, None) + self.last_alive.pop(csessid, None) if not self.last_alive: # no more ajax clients. Stop the keepalive self.keep_alive.stop() self.keep_alive = None - def lineSend(self, suid, data): + def lineSend(self, csessid, data): """ This adds the data to the buffer and/or sends it to the client as soon as possible. Args: - suid (int): Session id. + csessid (int): Session id. data (list): A send structure [cmdname, [args], {kwargs}]. """ - request = self.requests.get(suid) + request = self.requests.get(csessid) if request: # we have a request waiting. Return immediately. request.write(jsonify(data)) request.finish() - del self.requests[suid] + del self.requests[csessid] else: # no waiting request. Store data in buffer - dataentries = self.databuffer.get(suid, []) + dataentries = self.databuffer.get(csessid, []) dataentries.append(jsonify(data)) - self.databuffer[suid] = dataentries + self.databuffer[csessid] = dataentries - def client_disconnect(self, suid): + def client_disconnect(self, csessid): """ - Disconnect session with given suid. + Disconnect session with given csessid. Args: - suid (int): Session id. + csessid (int): Session id. """ - if suid in self.requests: - self.requests[suid].finish() - del self.requests[suid] - if suid in self.databuffer: - del self.databuffer[suid] + if csessid in self.requests: + self.requests[csessid].finish() + del self.requests[csessid] + if csessid in self.databuffer: + del self.databuffer[csessid] def mode_init(self, request): """ @@ -150,38 +149,32 @@ class WebClient(resource.Resource): request (Request): Incoming request. """ - suid = request.args.get('suid', ['0'])[0] + csessid = request.args.get('csessid') remote_addr = request.getClientIP() host_string = "%s (%s:%s)" % (_SERVERNAME, request.getRequestHostname(), request.getHost().port) - if suid == '0': - # creating a unique id hash string - suid = md5(str(time())).hexdigest() - self.databuffer[suid] = [] - sess = WebClientSession() - sess.client = self - sess.init_session("ajax/comet", remote_addr, self.sessionhandler) - sess.suid = suid - sess.sessionhandler.connect(sess) + sess = WebClientSession() + sess.client = self + sess.init_session("ajax/comet", remote_addr, self.sessionhandler) + sess.csessid = csessid + sess.sessionhandler.connect(sess) - self.last_alive[suid] = (time(), False) - if not self.keep_alive: - # the keepalive is not running; start it. - self.keep_alive = LoopingCall(self._keepalive) - self.keep_alive.start(_KEEPALIVE, now=False) + self.last_alive[csessid] = (time(), False) + if not self.keep_alive: + # the keepalive is not running; start it. + self.keep_alive = LoopingCall(self._keepalive) + self.keep_alive.start(_KEEPALIVE, now=False) - return jsonify({'msg': host_string, 'suid': suid}) + return jsonify({'msg': host_string, 'csessid': csessid}) def mode_keepalive(self, request): """ This is called by render_POST when the client is replying to the keepalive. """ - suid = request.args.get('suid', ['0'])[0] - if suid == '0': - return '""' - self.last_alive[suid] = (time(), False) + csessid = request.args.get('csessid')[0] + self.last_alive[csessid] = (time(), False) return '""' def mode_input(self, request): @@ -193,12 +186,10 @@ class WebClient(resource.Resource): request (Request): Incoming request. """ - suid = request.args.get('suid', ['0'])[0] - if suid == '0': - return '""' + csessid = request.args.get('csessid')[0] - self.last_alive[suid] = (time(), False) - sess = self.sessionhandler.session_from_suid(suid) + self.last_alive[csessid] = (time(), False) + sess = self.sessionhandler.session_from_csessid(csessid) if sess: sess = sess[0] cmdarray = json.loads(request.args.get('data')[0]) @@ -216,18 +207,16 @@ class WebClient(resource.Resource): request (Request): Incoming request. """ - suid = request.args.get('suid', ['0'])[0] - if suid == '0': - return '""' - self.last_alive[suid] = (time(), False) + csessid = request.args.get('csessid')[0] + self.last_alive[csessid] = (time(), False) - dataentries = self.databuffer.get(suid, []) + dataentries = self.databuffer.get(csessid, []) if dataentries: return dataentries.pop(0) - request.notifyFinish().addErrback(self._responseFailed, suid, request) - if suid in self.requests: - self.requests[suid].finish() # Clear any stale request. - self.requests[suid] = request + request.notifyFinish().addErrback(self._responseFailed, csessid, request) + if csessid in self.requests: + self.requests[csessid].finish() # Clear any stale request. + self.requests[csessid] = request return server.NOT_DONE_YET def mode_close(self, request): @@ -239,16 +228,13 @@ class WebClient(resource.Resource): request (Request): Incoming request. """ - suid = request.args.get('suid', ['0'])[0] - if suid == '0': - self.client_disconnect(suid) - else: - try: - sess = self.sessionhandler.session_from_suid(suid)[0] - sess.sessionhandler.disconnect(sess) - except IndexError: - self.client_disconnect(suid) - pass + csessid = request.args.get('csessid')[0] + try: + sess = self.sessionhandler.session_from_csessid(csessid)[0] + sess.sessionhandler.disconnect(sess) + except IndexError: + self.client_disconnect(csessid) + pass return '""' def render_POST(self, request): @@ -306,8 +292,8 @@ class WebClientSession(session.Session): Args: reason (str): Motivation for the disconnect. """ - self.client.lineSend(self.suid, ["connection_close", [reason], {}]) - self.client.client_disconnect(self.suid) + self.client.lineSend(self.csessid, ["connection_close", [reason], {}]) + self.client.client_disconnect(self.csessid) self.sessionhandler.disconnect(self) def data_out(self, **kwargs): @@ -363,7 +349,7 @@ class WebClientSession(session.Session): args[0] = parse_html(text, strip_ansi=nomarkup) # send to client on required form [cmdname, args, kwargs] - self.client.lineSend(self.suid, [cmd, args, kwargs]) + self.client.lineSend(self.csessid, [cmd, args, kwargs]) def send_prompt(self, *args, **kwargs): kwargs["options"].update({"send_prompt": True}) @@ -385,4 +371,4 @@ class WebClientSession(session.Session): """ if not cmdname == "options": #print "ajax.send_default", cmdname, args, kwargs - self.client.lineSend(self.suid, [cmdname, args, kwargs]) + self.client.lineSend(self.csessid, [cmdname, args, kwargs]) diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 4a99751ea4..f9eb93fc41 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -20,7 +20,7 @@ from evennia.utils.utils import make_iter, lazy_property from evennia.commands.cmdsethandler import CmdSetHandler from evennia.server.session import Session -BrowserSessionStore = importlib.import_module(settings.SESSION_ENGINE).SessionStore +ClientSessionStore = importlib.import_module(settings.SESSION_ENGINE).SessionStore _GA = object.__getattribute__ _SA = object.__setattr__ @@ -163,7 +163,6 @@ class ServerSession(Session): "Initiate to avoid AttributeErrors down the line" self.puppet = None self.player = None - self.browserid = None self.cmdset_storage_string = "" self.cmdset = CmdSetHandler(self, True) @@ -224,12 +223,14 @@ class ServerSession(Session): self.puppet = None self.cmdset_storage = settings.CMDSET_SESSION - if self.browserid: - # this is only set by a webclient inputcommand. - bsession = BrowserSessionStore(session_key=self.browserid) - bsession["logged_in"] = player.id # this also saves the bsession - bsession.save() - print ("serversession.login:", bsession.session_key) + if self.csessid: + # An existing client sessid is registered, thus a matching + # Client Session must also exist. Update it so the website + # can also see we are logged in. + csession = ClientSessionStore(session_key=self.browserid) + csession["logged_in"] = player.id + csession.save() + print ("serversession.login:", csession.session_key) # Update account's last login time. self.player.last_login = timezone.now() diff --git a/evennia/server/session.py b/evennia/server/session.py index 8efa713874..b8d491e03e 100644 --- a/evennia/server/session.py +++ b/evennia/server/session.py @@ -36,7 +36,7 @@ class Session(object): """ # names of attributes that should be affected by syncing. - _attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', + _attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', 'csessid', 'uname', 'logged_in', 'puid', 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', 'protocol_flags', 'server_data', "cmdset_storage_string") @@ -64,6 +64,8 @@ class Session(object): # unique id for this session self.sessid = 0 # no sessid yet + # client session id, if given by the client + self.csessid = None # database id for the user connected to this session self.uid = None # user name, for easier tracking of sessions diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 9c70540974..9f650c315e 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -593,16 +593,17 @@ class ServerSessionHandler(SessionHandler): return sessions[0] if len(sessions) == 1 else sessions sessions_from_character = sessions_from_puppet - def sessions_from_browserid(self, browserid): + def sessions_from_csessid(self, csessid): """ - Given a browserid, return all sessions having this id. + Given a cliend identification hash (for session types that offer them) return all sessions with + a matching hash. Args - browserid (str): The browserid hash + csessid (str): The session hash """ return [session for session in self.values() - if session.browserid and session.browserid == browserid] + if session.csessid and session.csessid == csessid] def announce_all(self, message): """ diff --git a/evennia/web/webclient/static/webclient/js/evennia.js b/evennia/web/webclient/static/webclient/js/evennia.js index 9deac7aea8..9a7bf9b0e2 100644 --- a/evennia/web/webclient/static/webclient/js/evennia.js +++ b/evennia/web/webclient/static/webclient/js/evennia.js @@ -217,20 +217,21 @@ An "emitter" object must have a function var ever_open = false; var websocket = null; var wsurl = window.wsurl; + var csessid = window.csessid; var connect = function() { if (websocket && websocket.readyState != websocket.CLOSED) { // No-op if a connection is already open. return; } - websocket = new WebSocket(wsurl); + websocket = new WebSocket(wsurl + '?' + csessid); // Handle Websocket open event websocket.onopen = function (event) { open = true; ever_open = true; Evennia.emit('connection_open', ["websocket"], event); - Evennia.msg('browser_sessid', [browser_sessid], {}); + Evennia.msg('csessid', [csessid], {}); }; // Handle Websocket close event websocket.onclose = function (event) { @@ -295,22 +296,22 @@ An "emitter" object must have a function // var AjaxCometConnection = function() { log("Trying ajax ..."); - var client_hash = '0'; + var open = false; var stop_polling = false; var is_closing = false; + var csessid = window.csessid; - // initialize connection and get hash + // initialize connection, send csessid var init = function() { $.ajax({type: "POST", url: "/webclientdata", async: true, cache: false, timeout: 50000, datatype: "json", - data: {mode: "init", suid: client_hash}, + data: {mode: "init", csessid: csessid}, success: function(data) { + open = true; data = JSON.parse(data); log ("connection_open", ["AJAX/COMET"], data); - Evennia.msg("browser_sessid", [browser_sessid], {}); - client_hash = data.suid; stop_polling = false; poll(); }, @@ -331,7 +332,7 @@ An "emitter" object must have a function async: true, cache: false, timeout: 30000, dataType: "json", data: {mode: inmode == null ? 'input' : inmode, - data: JSON.stringify(data), 'suid': client_hash}, + data: JSON.stringify(data), 'csessid': csessid}, success: function(req, stat, err) { stop_polling = false; }, @@ -351,7 +352,7 @@ An "emitter" object must have a function $.ajax({type: "POST", url: "/webclientdata", async: true, cache: false, timeout: 60000, dataType: "json", - data: {mode: 'receive', 'suid': client_hash}, + data: {mode: 'receive', 'csessid': csessid}, success: function(data) { // log("ajax data received:", data); if (data[0] === "ajax_keepalive") { @@ -393,7 +394,7 @@ An "emitter" object must have a function // Kill the connection and do house cleaning on the server. var close = function webclient_close(){ - if (is_closing || client_hash === '0') { + if (is_closing || !(open)) { // Already closed or trying to close. return; } @@ -406,11 +407,11 @@ An "emitter" object must have a function cache: false, timeout: 50000, dataType: "json", - data: {mode: 'close', 'suid': client_hash}, + data: {mode: 'close', 'csessid': csessid}, success: function(data){ is_closing = false; - client_hash = '0'; + open = false; Evennia.emit("connection_close", ["AJAX/COMET"], {}); log("AJAX/COMET connection closed cleanly.") }, @@ -419,13 +420,13 @@ An "emitter" object must have a function Evennia.emit("connection_error", ["AJAX/COMET close error"], err); // Also emit a close event so that the COMET API mirrors the websocket API. Evennia.emit("connection_close", ["AJAX/COMET close unclean"], err); - client_hash = '0'; + open = false; } }); }; var isOpen = function () { - return !(is_closing || client_hash === '0'); + return !(is_closing || !(open)); } // init diff --git a/evennia/web/webclient/templates/webclient/base.html b/evennia/web/webclient/templates/webclient/base.html index f9beecc365..4492357bdc 100644 --- a/evennia/web/webclient/templates/webclient/base.html +++ b/evennia/web/webclient/templates/webclient/base.html @@ -36,9 +36,9 @@ JQuery available. {% endif %} {% if browser_sessid %} - var browser_sessid = "{{browser_sessid}}"; + var csessid = "{{browser_sessid}}"; {% else %} - var browser_sessid = false; + var csessid = false; {% endif %} {% if websocket_url %} diff --git a/evennia/web/webclient/views.py b/evennia/web/webclient/views.py index 26fc72fed8..3a877462e0 100644 --- a/evennia/web/webclient/views.py +++ b/evennia/web/webclient/views.py @@ -18,27 +18,26 @@ def webclient(request): """ print ("webclient session:", request.session.session_key, request.user, request.user.is_authenticated()) - browser_session = request.session - browserid = request.session.session_key + csession = request.session + csessid = request.session.session_key player = request.user # check if user has authenticated to website if player.is_authenticated(): print ("webclient: player auth, trying to connect sessions") # Try to login all the player's webclient sessions - only # unloggedin ones will actually be logged in. - for session in SESSION_HANDLER.sessions_from_browserid(browserid): + for session in SESSION_HANDLER.sessions_from_csessid(csessid): print ("session to connect:", session) if session.protocol_key in ("websocket", "ajax/comet"): SESSION_HANDLER.login(session, player) - session.browserid = browser_session.session_key - browser_session["logged_in"] = player.id - elif browser_session.get("logged_in"): + csession["logged_in"] = player.id + elif csession.get("logged_in"): # The webclient has previously registered a login to this browser_session print ("webclient: browser_session logged in, trying to login") - player = PlayerDB.objects.get(browser_session.get("uid")) + player = PlayerDB.objects.get(csession.get("logged_in")) login(player, request) else: - browser_session["logged_in"] = False + csession["logged_in"] = False # make sure to store the browser session's hash so the webclient can get to it pagevars = {'browser_sessid': request.session.session_key} diff --git a/evennia/web/website/views.py b/evennia/web/website/views.py index ab6853eb8c..c28d5ea275 100644 --- a/evennia/web/website/views.py +++ b/evennia/web/website/views.py @@ -26,27 +26,27 @@ def page_index(request): # handle webclient-website shared login - browser_session = request.session - browserid = request.session.session_key + csession = request.session + csessid = request.session.session_key player = request.user # check if user has authenticated to website if player.is_authenticated(): # Try to login all the player's webclient sessions - only # unloggedin ones will actually be logged in. print "website: player auth, trying to connect sessions" - for session in SESSION_HANDLER.sessions_from_browserid(browserid): + for session in SESSION_HANDLER.sessions_from_csessid(csessid): print "session to connect:", session if session.protocol_key in ("websocket", "ajax/comet"): SESSION_HANDLER.login(session, player) - session.browserid = browser_session.session_key - browser_session["logged_in"] = player.id - elif browser_session.get("logged_in"): - # The webclient has previously registered a login to this browser_session - print "website: browser_session logged in, trying to login" - player = PlayerDB.objects.get(id=browser_session.get("logged_in")) + session.csessid = csession.session_key + csession["logged_in"] = player.id + elif csession.get("logged_in"): + # The webclient has previously registered a login to this csession + print "website: csession logged in, trying to login" + player = PlayerDB.objects.get(id=csession.get("logged_in")) login(request, player) else: - browser_session["logged_in"] = None + csession["logged_in"] = None print ("website session:", request.session.session_key, request.user, request.user.is_authenticated())