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