Getting an improved version of the shared session system vaguely in shape.

This commit is contained in:
Griatch 2016-05-31 20:17:07 +02:00
parent a31441b3ce
commit eebd41f46d
11 changed files with 143 additions and 130 deletions

View file

@ -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

View file

@ -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):
"""

View file

@ -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):

View file

@ -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])

View file

@ -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()

View file

@ -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

View file

@ -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):
"""

View file

@ -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

View file

@ -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 %}

View file

@ -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}

View file

@ -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())