Added support for GMCP out-of-band messaging, as a backup handler to MSDP. Starting to rework the oob system to be more straightforward to follow and understand.

This commit is contained in:
Griatch 2015-02-11 20:58:37 +01:00
parent f1b6a4e212
commit eda15ccc45
9 changed files with 416 additions and 555 deletions

View file

@ -0,0 +1,238 @@
"""
Telnet OOB (Out of band communication)
This implements the following telnet oob protocols:
MSDP (Mud Server Data Protocol)
GMCP (Generic Mud Communication Protocol)
This implements the MSDP protocol as per
http://tintin.sourceforge.net/msdp/ and the GMCP protocol as per
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
Following the lead of KaVir's protocol snippet, we first check if
client supports MSDP and if not, we fallback to GMCP with a MSDP
header where applicable.
OOB manages out-of-band
communication between the client and server, for updating health bars
etc. See also GMCP which is another standard doing the same thing.
"""
import re
import json
from evennia.utils.utils import to_str
# MSDP-relevant telnet cmd/opt-codes
MSDP = chr(69)
MSDP_VAR = chr(1)
MSDP_VAL = chr(2)
MSDP_TABLE_OPEN = chr(3)
MSDP_TABLE_CLOSE = chr(4)
MSDP_ARRAY_OPEN = chr(5)
MSDP_ARRAY_CLOSE = chr(6)
GMCP = chr(200)
IAC = chr(255)
SB = chr(250)
SE = chr(240)
force_str = lambda inp: to_str(inp, force_string=True)
# pre-compiled regexes
# returns 2-tuple
msdp_regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_ARRAY_OPEN,
MSDP_ARRAY_CLOSE))
# returns 2-tuple (may be nested)
msdp_regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_TABLE_OPEN,
MSDP_TABLE_CLOSE))
msdp_regex_var = re.compile(MSDP_VAR)
msdp_regex_val = re.compile(MSDP_VAL)
# Msdp object handler
class Telnet_OOB(object):
"""
Implements the MSDP and GMCP protocols.
"""
def __init__(self, protocol):
"""
Initiates by storing the protocol
on itself and trying to determine
if the client supports MSDP.
"""
self.protocol = protocol
self.protocol.protocol_flags['OOB'] = False
self.MSDP = False
self.GMCP = False
# detect MSDP first
self.protocol.negotiationMap[MSDP] = self.data_in
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
self.oob_reported = {}
def no_msdp(self, option):
"No msdp supported or wanted"
# no msdp, check GMCP
self.protocol.negotiationMap[GMCP] = self.data_in
self.protocol.will(GMCP).addCallbacks(self.do_oob, self.no_oob)
def do_msdp(self, option):
"MSDP supported by client"
self.MSDP = True
self.protocol.protocol_flags['OOB'] = True
self.protocol.handshake_done()
def no_gmcp(self, option):
"Neither MSDP nor GMCP supported"
self.protocol.handshake_done()
def do_gmcp(self, option):
"""
Called when client confirms that it can do MSDP or GMCP.
"""
self.GMCP = True
self.protocol.protocol_flags['OOB'] = True
self.protocol.handshake_done()
# encoders
def encode_msdp(self, cmdname, *args, **kwargs):
"""
handle return data from cmdname by converting it to
a proper msdp structure. These are the combinations we
support:
cmdname string -> cmdname string
cmdname *args -> cmdname MSDP_ARRAY
cmdname **kwargs -> cmdname MSDP_TABLE
OBS - whereas there are also definitions for making arrays and tables in
the specification, these are not actually used in the default
msdp commands -- returns are implemented as simple lists or
named lists (our name for them here, these un-bounded
structures are not named in the specification). So for now,
this routine will not explicitly create arrays nor tables,
although there are helper methods ready should it be needed in
the future.
"""
msdp_string = ""
if args:
if len(args) == 1:
msdp_string = "%s %s" % (cmdname.upper(), args[0])
else:
msdp_string = "%s%s%s%s" % (MSDP_VAR, cmdname.upper(), "".join(
"%s%s" % (MSDP_VAL, val) for val in args))
elif kwargs:
msdp_string = "%s%s%s" % (MSDP_VAR. cmdname.upper(), "".join(
["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) for key, val in kwargs.items()]))
return msdp_string
def encode_gmcp(self, cmdname, *args, **kwargs):
"""
Gmcp messages are on one of the following outgoing forms:
cmdname string -> cmdname string
cmdname *args -> cmdname [arg, arg, arg, ...]
cmdname **kwargs -> cmdname {key:arg, key:arg, ...}
cmdname is generally recommended to be a string on the form
Module.Submodule.Function
"""
if args:
gmcp_string = "%s %s" % (cmdname, json.dumps(args))
elif kwargs:
gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs))
return gmcp_string
def decode_msdp(self, data):
"""
Decodes incoming MSDP data
cmdname, var --> cmdname arg
cmdname, array --> cmdname *args
cmdname, table --> cmdname **kwargs
"""
tables = {}
arrays = {}
variables = {}
if hasattr(data, "__iter__"):
data = "".join(data)
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
# decode
for key, table in msdp_regex_table.findall(data):
tables[key] = {}
for varval in msdp_regex_var.split(table):
parts = msdp_regex_val.split(varval)
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
for key, array in msdp_regex_array.findall(data):
arrays[key] = []
for val in msdp_regex_val.split(array):
arrays[key].append(val)
arrays[key] = tuple(arrays[key])
for varval in msdp_regex_var.split(msdp_regex_array.sub("", msdp_regex_table.sub("", data))):
# get remaining varvals after cleaning away tables/arrays
parts = msdp_regex_val.split(varval)
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
# send to the sessionhandler
if data:
for varname, var in variables.items():
# a simple function + argument
self.protocol.data_in(oob=(varname, var, {}))
for arrayname, array in arrays.items():
# we assume the array are multiple arguments to the function
self.protocol.data_in(oob=(arrayname, array, {}))
for tablename, table in tables.items():
# we assume tables are keyword arguments to the function
self.protocol.data_in(oob=(tablename, (), table))
def decode_gmcp(self, data):
"""
Decodes incoming GMCP data on the form 'varname <structure>'
cmdname string -> cmdname arg
cmdname [arg, arg,...] -> cmdname *args
cmdname {key:arg, key:arg, ...} -> cmdname **kwargs
"""
if data:
splits = data.split(" ", 1)
cmdname = splits[0]
if len(splits) < 2:
self.protocol.data_in(oob=(cmdname, (), {}))
else:
struct = json.loads(splits[1])
self.protocol.data_in(oob=(cmdname,
struct if isinstance(struct, list) else (),
struct if isinstance(struct, dict) else {}))
# access methods
def data_out(self, cmdname, *args, **kwargs):
"""
Return a msdp-valid subnegotiation across the protocol.
"""
if self.MSDP:
encoded_oob = force_str(self.encode_msdp(cmdname, *args, **kwargs))
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
else:
encoded_oob = force_str(self.encode_gmcp(cmdname, *args, **kwargs))
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
def data_in(self, data):
"""
Send oob data to Evennia. The self.decode_* methods send to
protocol.data_in() themselves.
"""
if self.MSDP:
self.decode_msdp(data)
else:
self.decode_gmcp(data)