Re: mccp module
will guaraldi <[email protected]> Mon, 13 Dec 2004 12:24:00 -0600 (CST)
| Newsgroups | gmane.comp.games.mud.client.lyntin |
|---|---|
| Message-ID | <[email protected]> |
You're right--that's a total screwup on my part when I re-designed the network module. It really requires a re-write of the module and handling of incoming and outgoing data. However, I wrote an MCCP module based on... hrm... based on Conan's code. Though I didn't have a good place to test it since I don't play on any MCCP-enabled muds. Anyhow, it's attached. See if this module helps your success rate with MCCP. /will On Sun, 12 Dec 2004, S.Aeschbacher wrote: > Hi > > I hacked together a module which would offer MCCP functionality. > On the server I use it SOMETIMES works (with MCCPv2). > > One problem I see is the following: > According to the specification of MCCP [1], the server starts > to send compressed data directly after having sent the telnet > option (IAC SB COMPRESS2 IAC SE for MCCPv2) which indicates the start > of the compressed data. So this option can be at the start of a > chunk of data that is compressed (all except the option). > The problem is, _filterIncomingData has already been called when > the handleNego method is running. When this first part is not sent > through the decompressor object, the whole stream is out of > sync and nothing works... > > At the moment I don't see how I can repass the data through the > decompressor. Any hints or ideas? > Or am I searching in the wrong place? > > regards > > Stefan > > > [1] (http://mccp.afkmud.com/protocol.html) > -- whatever it is, you can find it at http://www.bluesock.org/~willg/ except Will--you can only see him in real life.
_mccpplugin.py
(text/x-python, 13.3 KB)
"""
Client decompression module for the mud client compression protocol.
See http://homepages.ihug.co.nz/~icecube/compress/ for more details.
Original C version by Oliver Jowett <icecube$ihug.co.nz>.
Python version by Conan Brink <conanb$psnw.com>.
Demangle addresses as needed.
This python code is placed in the public domain, as was the original
C source code from which it is derived.
The code was then converted into a Lyntin plugin module for Lyntin 4.*
by Will Guaraldi <willg$bluesock.org>.
Notes:
- tested with lensmoor.org 3500 (uses v2)
"""
__author__ = "Will Guaraldi (et al)"
__version__ = "0.5 (23 March, 2004)"
__description__ = "MCCP module for Lyntin. Supports v1 and v2."
from lyntin import exported, net
from lyntin.modules import modutils
import zlib
# The mc_state class represents the current compression state of a connection.
#
# Reading / writing:
#
# Reading from the server connection must go through the decompressor at
# all times. The decompressor handles both negotiation and decompression
# transparently - it receives input directly from the server, then provides
# the main client code with decompressed data, hiding all protocol details.
#
# When data is received from the mud server, call receive, passing it
# the data read. It is VITAL that ALL data read is passed to the
# decompressor - including data with embedded NULs!
#
# After receive has been called, call pending() to see if any decompressed
# data is available. It returns the number of bytes pending.
#
# If there is pending data waiting, call get to retrieve it.
# Your client can now process this data as if it had been directly read
# from the server.
#
# Regularly call response. If non-NULL, you need to write the returned
# string to the mud server. This is needed when the decompressor
# is negotiating compression with the server. When called, response
# clears any pending string, so be sure to save its return value!
#
# Status information:
#
# stats returns two values: the number of compressed bytes read, and the
# number of bytes that they decompressed to.
#
# compressing returns non-0 if the connection is currently using compression.
# Telnet values we're interested in
IAC = chr(255)
DONT = chr(254)
DO = chr(253)
WONT = chr(252)
WILL = chr(251)
SB = chr(250)
SE = chr(240)
TELOPT_COMPRESS = chr(85)
TELOPT_COMPRESS2 = chr(86)
# We say DO COMPRESS2 to WILL COMPRESS2, then DONT COMPRESS to WILL COMPRESS
# -or-
# We say DO COMPRESS to WILL COMPRESS if it arrives before any COMPRESS2.
#
# Later the server sends IAC SB COMPRESS IAC SE (v2) or IAC SB COMPRESS WILL
# SE (v1), and immediately following that, begins compressing.
#
# Compression ends when the zlib stream ends, which causes the remaining
# uncompressed data to be in the state.dec.unused_data, from which we copy
# it back into the inbuf to feed into the telnet state machine.
will_v1 = "%s%s%s" % (IAC, WILL, TELOPT_COMPRESS)
do_v1 = "%s%s%s" % (IAC, DO, TELOPT_COMPRESS)
dont_v1 = "%s%s%s" % (IAC, DONT, TELOPT_COMPRESS)
on_v1 = "%s%s%s%s%s" % (IAC, SB, TELOPT_COMPRESS, WILL, SE)
will_v2 = "%s%s%s" % (IAC, WILL, TELOPT_COMPRESS2)
do_v2 = "%s%s%s" % (IAC, DO, TELOPT_COMPRESS2)
on_v2 = "%s%s%s%s%s" % (IAC, SB, TELOPT_COMPRESS2, IAC, SE)
def write_socket(ses, data):
sock = ses._socket.write(data, convert=0)
# state object
class mc_state:
def __init__(self):
self.stream = 0
self.inbuf = ""
self.outbuf = ""
self.comp = 0
self.uncomp = 0
self.resp_v1 = 0
self.resp_v2 = 0
self.got_v2 = 0
self.dec = None
def receive(self, data):
"""
Perform decompression and negotiation on some received data.
"""
# First, we append everything we got onto the end of self.inbuf
self.inbuf = self.inbuf + data
# Now, as long as there's any data left, we want to feed it through
# the decompressor (if it's compressed), react to any telnet negotiation
# that we recognize, save any incomplete telnet negotiation sequences,
# and pass anything else intact
while len(self.inbuf) > 0:
if self.stream:
# We're compressed, so let's decompress it
decbytes = self.dec.decompress(self.inbuf)
self.uncomp = self.uncomp + len(decbytes)
self.outbuf = self.outbuf + decbytes
if len(self.dec.unused_data) > 0:
# We ran out of compressed data and have some uncompressed
# data behind it. Save that uncompressed stuff in case there's
# more telnet stuff in it. Nuke the decompression object so
# we know we're in an uncompressed state.
self.comp = self.comp + len(self.inbuf) - len(self.dec.unused_data)
self.inbuf = self.dec.unused_data
self.stream = 0
del self.dec
self.dec = None
else:
self.comp = self.comp + len(self.inbuf)
self.inbuf = ""
else:
# We're not compressed. Check for telnet stuff
iacindex = self.inbuf.find(IAC)
if (iacindex >= 0):
# Found something interesting.
# First give everything before it to the outbuf.
self.outbuf = self.outbuf + self.inbuf[:iacindex]
self.inbuf = self.inbuf[iacindex:]
if len(self.inbuf) == 1:
# We have to save this lone IAC byte until later
# because we don't yet know what sequence it starts
break;
if self.inbuf[1] == IAC:
# Pass this on unaltered.
self.outbuf = self.outbuf + self.inbuf[0:2]
self.inbuf = self.inbuf[2:]
elif self.inbuf[1] == WILL:
if len(self.inbuf) == 2:
# We have to save these two bytes because we don't
# yet know which option the server wants to negotiate
break;
if self.inbuf[2] == TELOPT_COMPRESS:
# Reject if we already got WILL COMPRESS2
# Accept if we didn't
if self.resp_v2:
self.resp_v1 = -1
else:
self.resp_v1 = 1
elif self.inbuf[2] == TELOPT_COMPRESS2:
# Note that we got it and want to answer it.
self.resp_v2 = 1
self.got_v2 = 1
else:
# Not for us. Pass it on.
self.outbuf = self.outbuf + self.inbuf[0:3]
# Remove it from the datastream
self.inbuf = self.inbuf[3:]
elif self.inbuf[1] == SB:
seindex = self.inbuf.find(SE)
if seindex < 0:
# Protect ourselves against endless streaming from
# an evil mud. No sub-negotiation deserves to be
# longer than 1 kilobyte.
if len(self.inbuf) > 1024:
self.outbuf = self.outbuf + self.inbuf[0:2]
self.inbuf = self.inbuf[2:]
else:
# Save for later. This code hasn't ended yet.
break;
if self.inbuf[0:seindex + 1] == on_v1:
# Success! We have gotten the sequence
# that turns on compression with version 1
self.stream = 1
self.dec = zlib.decompressobj(15)
# Now chop off the magic sequence so we have
# a nice string to feed to the decompressor.
self.inbuf = self.inbuf[5:]
elif self.inbuf[0:seindex + 1] == on_v2:
# Success! We have gotten the sequence
# that turns on compression with version 2
self.stream = 1
self.dec = zlib.decompressobj(15)
# Now chop off the magic sequence so we have
# a nice string to feed to the decompressor.
self.inbuf = self.inbuf[5:]
else:
# It wasn't either of our sub-negotiation sequences.
# Pass it on to the client.
self.outbuf = self.outbuf + self.inbuf[0:seindex+1]
self.inbuf = self.inbuf[seindex+1:]
else:
if len(self.inbuf) < 3:
# save for later
break
# send the telnet option to someone else
self.outbuf = self.outbuf + self.inbuf[:3]
self.inbuf = self.inbuf[3:]
else:
# No telnet stuff left in the string, and it's not even
# compressed. Pass the whole thing to the outbuf
self.outbuf = self.outbuf + self.inbuf
self.inbuf = ""
def pending(self):
"""
Return the number of pending decompressed bytes that can currently
be read by mudcompress_get
"""
return len(self.outbuf)
def get(self):
"""
Read decompressed data from the decompressor.
"""
s = self.outbuf
self.outbuf = ""
return s
def stats(self):
"""
Return the number of compressed bytes read and the number of bytes they
expanded to, for this decompressor.
"""
return (self.comp, self.uncomp)
def response(self, ses):
"""
Check for a negotiation response. If this returns None, no output is
needed. If it returns anything else, that string should be sent to the
mud server. Calling this function clears the pending string (so be sure
to save the result).
"""
if self.resp_v1 == 1:
self.resp_v1 = 0
write_socket(ses, do_v1)
return
if self.resp_v1 == -1:
self.resp_v1 = 0
write_socket(ses, dont_v1)
return
if self.resp_v2:
self.resp_v2 = 0
write_socket(ses, do_v2)
return
def compressing(self):
"""
Return true (non-0) if this decompressor has successfully negotiated
compression and is currently performing decompression.
"""
return self.stream
# -------------------------------------------------------------
# end of material from the mccpDecompress module
# -------------------------------------------------------------
from lyntin import manager
class MCCPManager(manager.Manager):
def __init__(self):
self._states = {}
def connectHandler(self, args):
ses = args["session"]
self._states[ses] = mc_state()
def netReadHandler(self, args):
ses = args["session"]
data = args["dataadj"]
if not data:
return data
mcstate = self._states[ses]
mcstate.receive(data)
mcstate.response(ses)
data = mcstate.get()
return data
commands_dict = {}
def mccpstats_cmd(ses, args, input):
"""
Tells you the compressed and decompressed stats for the session
thus far.
category: mccp
"""
global mccpm
if not mccpm._states.has_key(ses):
exported.write_message("mccpstats: MCCP is not enabled for this session.")
return
mcstate = mccpm._states[ses]
if mcstate.got_v2:
comp = "(V2)"
else:
comp = "(V1)"
exported.write_message("mccpstats: MCCP is enabled for this session. %s" % comp)
c, d = mcstate.stats()
exported.write_message("mccpstats: %d bytes -> %d decompressed" % (c, d))
return
commands_dict["mccpstats"] = (mccpstats_cmd, "")
mccpm = None
def load():
global mccpm
mccpm = MCCPManager()
# register with connect hook to build an mc_state object per session
exported.hook_register("connect_hook", mccpm.connectHandler)
# register with net_read_data_filter hook to decompress incoming data
exported.hook_register("net_read_data_filter", mccpm.netReadHandler)
modutils.load_commands(commands_dict)
def unload():
global mccpm
exported.hook_unregister("connect_hook", mccpm.connectHandler)
exported.hook_unregister("net_read_data_filter", mccpm.netReadHandler)
modutils.unload_commands(commands_dict)