From 31687b8a05054e5c02d71c79da375f864dce2881 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 14 Aug 2014 10:28:50 +0200 Subject: [PATCH] Made telnet protocols resync with server once their handshakes are complete. Also changed default (pre-TTYPE) to be ansi+xterm256. Set a 5-second timeout for handshakes. This pertains to issue #434. --- src/server/amp.py | 6 ++++- src/server/portal/mccp.py | 2 ++ src/server/portal/msdp.py | 3 ++- src/server/portal/mssp.py | 2 ++ src/server/portal/portalsessionhandler.py | 15 ++++++++++- src/server/portal/telnet.py | 31 ++++++++++++++++++----- src/server/portal/ttype.py | 3 +++ src/server/sessionhandler.py | 13 +++++++++- 8 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/server/amp.py b/src/server/amp.py index e529f7c208..5a07362da0 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -39,6 +39,7 @@ SDISCONNALL = chr(6) # server session disconnect all SSHUTD = chr(7) # server shutdown SSYNC = chr(8) # server session sync SCONN = chr(9) # server creating new connectiong (for irc/imc2 bots etc) +PCONNSYNC = chr(10) # portal post-syncing a session MAXLEN = 65535 # max allowed data length in AMP protocol _MSGBUFFER = defaultdict(list) @@ -412,6 +413,9 @@ class AMPProtocol(amp.AMP): # create a new session and sync it server_sessionhandler.portal_connect(data) + elif operation == PCONNSYNC: #portal_session_sync + server_sessionhandler.portal_session_sync(data) + elif operation == PDISCONN: # portal_session_disconnect # session closed from portal side self.factory.server.sessions.portal_disconnect(sessid) @@ -422,7 +426,7 @@ class AMPProtocol(amp.AMP): # contains a dict {sessid: {arg1:val1,...}} # representing the attributes to sync for each # session. - server_sessionhandler.portal_session_sync(data) + server_sessionhandler.portal_sessions_sync(data) else: raise Exception("operation %(op)s not recognized." % {'op': operation}) return {} diff --git a/src/server/portal/mccp.py b/src/server/portal/mccp.py index 0e07f23e97..b219d6bd66 100644 --- a/src/server/portal/mccp.py +++ b/src/server/portal/mccp.py @@ -54,6 +54,7 @@ class Mccp(object): if hasattr(self.protocol, 'zlib'): del self.protocol.zlib self.protocol.protocol_flags['MCCP'] = False + self.protocol.handshake_done() def do_mccp(self, option): """ @@ -63,3 +64,4 @@ class Mccp(object): self.protocol.protocol_flags['MCCP'] = True self.protocol.requestNegotiation(MCCP, '') self.protocol.zlib = zlib.compressobj(9) + self.protocol.handshake_done() diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 7d05a3e406..aa36bd65d8 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -60,13 +60,14 @@ class Msdp(object): def no_msdp(self, option): "No msdp supported or wanted" - pass + self.protocol.handshake_done() def do_msdp(self, option): """ Called when client confirms that it can do MSDP. """ self.protocol.protocol_flags['MSDP'] = True + self.protocol.handshake_done() def evennia_to_msdp(self, cmdname, *args, **kwargs): """ diff --git a/src/server/portal/mssp.py b/src/server/portal/mssp.py index 3bda35d696..96b1a9e056 100644 --- a/src/server/portal/mssp.py +++ b/src/server/portal/mssp.py @@ -47,6 +47,7 @@ class Mssp(object): """ This is the normal operation. """ + self.protocol.handshake_done() pass def do_mssp(self, option): @@ -181,3 +182,4 @@ class Mssp(object): # send to crawler by subnegotiation self.protocol.requestNegotiation(MSSP, varlist) + self.protocol.handshake_done() diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index ba792ccaf0..9ff5d122d7 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -2,7 +2,7 @@ Sessionhandler for portal sessions """ import time -from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN +from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PSYNC, PCONNSYNC _MOD_IMPORT = None @@ -54,6 +54,19 @@ class PortalSessionHandler(SessionHandler): self.portal.amp_protocol.call_remote_ServerAdmin(sessid, operation=PCONN, data=sessdata) + def sync(self, session): + """ + Called by the protocol of an already connected session. This + can be used to sync the session info in a delayed manner, + such as when negotiation and handshakes are delayed. + """ + if session.sessid: + # only use if session already has sessid (i.e. has already connected) + sessdata = session.get_sync_data() + if self.portal.amp_protocol: + self.portal.amp_protocol.call_remote_ServerAdmin(session.sessid, + operation=PCONNSYNC, + data=sessdata) def disconnect(self, session): """ diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index 1d5c237d3f..8612cd0a95 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -30,6 +30,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ # initialize the session self.iaw_mode = False + self.handshakes = 4 # ttype, mccp, mssp, msdp client_address = self.transport.client self.init_session("telnet", client_address, self.factory.sessionhandler) # negotiate ttype (client info) @@ -43,6 +44,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): self.msdp = msdp.Msdp(self) # add this new connection to sessionhandler so # the Server becomes aware of it. + self.sessionhandler.connect(self) # This is a fix to make sure the connection does not # continue until the handshakes are done. This is a @@ -54,9 +56,26 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # to their defaults since sessionhandler.connect will sync # before the handshakes have had time to finish. Keeping this patch # until coming up with a more elegant solution /Griatch + from src.utils.utils import delay - delay(1, callback=self.sessionhandler.connect, retval=self) - #self.sessionhandler.connect(self) + delay(5, callback=self.handshake_done, retval=True) + + def handshake_done(self, force=False): + """ + This is called by all telnet extensions once they are finished. + When all have reported, a sync with the server is performed. + The system will force-call this sync after a small time to handle + clients that don't reply to handshakes at all. + info - debug text from the protocol calling + """ + if self.handshakes > 0: + if force: + self.sessionhandler.sync(self) + return + self.handshakes -= 1 + if self.handshakes <= 0: + # do the sync + self.sessionhandler.sync(self) def enableRemote(self, option): """ @@ -208,10 +227,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # parse **kwargs, falling back to ttype if nothing is given explicitly ttype = self.protocol_flags.get('TTYPE', {}) - xterm256 = kwargs.get("xterm256", ttype and ttype.get('256 COLORS', False)) - useansi = kwargs.get("ansi", ttype and ttype.get('ANSI', False)) + xterm256 = kwargs.get("xterm256", ttype.get('256 COLORS', False) if ttype.get("init_done") else True) + useansi = kwargs.get("ansi", ttype and ttype.get('ANSI', False) if ttype.get("init_done") else True) raw = kwargs.get("raw", False) - nomarkup = kwargs.get("nomarkup", not (xterm256 or useansi) or not ttype.get("init_done")) + nomarkup = kwargs.get("nomarkup", not (xterm256 or useansi)) prompt = kwargs.get("prompt") if prompt: @@ -229,5 +248,5 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): else: # we need to make sure to kill the color at the end in order # to match the webclient output. - # print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self) + #print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self), "nomarkup: %s, xterm256: %s" % (nomarkup, xterm256) self.sendLine(ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=xterm256)) diff --git a/src/server/portal/ttype.py b/src/server/portal/ttype.py index c60dc32fb9..49b43a66f6 100644 --- a/src/server/portal/ttype.py +++ b/src/server/portal/ttype.py @@ -54,6 +54,7 @@ class Ttype(object): Callback if ttype is not supported by client. """ self.protocol.protocol_flags['TTYPE']["init_done"] = True + self.protocol.handshake_done() def will_ttype(self, option): """ @@ -139,4 +140,6 @@ class Ttype(object): self.protocol.protocol_flags['TTYPE']['init_done'] = True # print "TTYPE final:", self.protocol.protocol_flags['TTYPE'] + # we must sync ttype once it'd done + self.protocol.handshake_done() self.ttype_step += 1 diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index ac2d091b22..196125546a 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -38,6 +38,7 @@ SDISCONNALL = chr(6) # server session disconnect all SSHUTD = chr(7) # server shutdown SSYNC = chr(8) # server session sync SCONN = chr(9) # server portal connection (for bots) +PCONNSYNC = chr(10) # portal post-syncing session # i18n from django.utils.translation import ugettext as _ @@ -210,6 +211,16 @@ class ServerSessionHandler(SessionHandler): self.sessions[sess.sessid] = sess sess.data_in(CMD_LOGINSTART) + def portal_session_sync(self, portalsessiondata): + """ + Called by Portal when it wants to update a single session (e.g. + because of all negotiation protocols have finally replied) + """ + sessid = portalsessiondata.get("sessid") + session = self.sessions.get(sessid) + if session: + session.load_sync_data(portalsessiondata) + def portal_disconnect(self, sessid): """ Called by Portal when portal reports a closing of a session @@ -227,7 +238,7 @@ class ServerSessionHandler(SessionHandler): session.disconnect() del self.sessions[session.sessid] - def portal_session_sync(self, portalsessions): + def portal_sessions_sync(self, portalsessions): """ Syncing all session ids of the portal with the ones of the server. This is instantiated by the portal when reconnecting.