net.py :  » Development » Lyntin » lyntin-4.2 » lyntin » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Development » Lyntin 
Lyntin » lyntin 4.2 » lyntin » net.py
#########################################################################
# This file is part of Lyntin.
#
# Lyntin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Lyntin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# copyright (c) Free Software Foundation 2001-2007
#
# $Id: net.py,v 1.19 2007/07/24 00:39:03 willhelm Exp $
#########################################################################
"""
This holds the SocketCommunicator class which handles socket
connections with a mud and polling the connection for data.

X{bell_hook}::

   When the mud sends a bell character, we spam this hook.  Typically
   the ui's will register with this hook and handle the bell however
   they see fit.

   Arg mapping: { "session": Session }

   session - the session that received the bell


X{prompt_hook}::

   We try to do some prompt detection and separate prompts into their
   own events (separate from mud data).

   Arg mapping: { "session": Session, "prompt": string }

   session - the Session that this prompt came from

   prompt - the prompt string


X{connect_hook}::

   This hook gets spammed every time we make a successful connection.

   Arg mapping: { "session": Session, "host": String, "port": int }

   session - the Session object for this connection

   host - the host we connected to

   port - the port number for the host


X{net_read_data_filter}::

   This allows you to filter incoming data before it passes through
   Lyntin.  If you were going to write an MCCP module, this is the
   hook it would register with to decompress incoming mud data.

   Functions that register with this hook should return the dataadj
   if they did nothing or the adjusted dataadj if they transformed it.
   Look at the user_filter_hook examples in the alias and speedwalk
   modules.

   Arg mapping: { "session": Session, "data": String, "dataadj": String }

   session - the Session that this data came from

   data - the original data we got from the mud

   dataadj - the data the previous function in the hook returned


X{net_write_data_filter}::

   This allows you to filter outgoing data after it has passed through
   Lyntin and just before it gets sent out on the socket.  If you were 
   going to write an MCCP module, this is the hook it would register 
   with to compress outgoing mud data.

   Functions that register with this hook should return the dataadj
   if they did nothing or the adjusted dataadj if they transformed it.
   Look at the user_filter_hook examples in the alias and speedwalk
   modules.

   Arg mapping: { "session": Session, "data": String, "dataadj": String }

   session - the Session that this data is going to

   data - the original data we wanted to send to the mud

   dataadj - the data the previous function in the hook returned


X{net_handle_telnet_option}::

   There are a series of Telnet options that Lyntin doesn't handle.
   So we allow module writers to handle them if they so desire.
   The data argument is the telnet option string.  So if they
   send us 255 251 24, then that's what you're getting.  We handle
   all the buffering of telnet option stuff--so you needn't worry
   about that.

   We send along the IAC DO TERMTYPE kinds of things as well as the
   IAC SB blah blah IAC SE kinds of things.  This is a handler--so
   if you've handled it, raise an exported.StopSpammingException() .

   Arg mapping: { "session": Session, "data": String }

   session - the Session that this telnet option came from

   data - the telnet option itself

"""
import socket, select, re, os

from lyntin import event,config,exported
from lyntin.ui import message

### --------------------------------------------
### CONSTANTS
### --------------------------------------------

# reverse lookup allowing us to see what's going on more easily
# when we're debugging.
# for a list of telnet options: http://www.freesoft.org/CIE/RFC/1700/10.htm
CODES = {255: "IAC",
         254: "DON'T",
         253: "DO",
         252: "WON'T",
         251: "WILL",
         250: "SB",
         249: "GA",
         240: "SE",
         239: "TELOPT_EOR",
         0:   "<IS>",
         1:   "[<ECHO> or <SEND/MODE>]",
         3:   "<SGA>",
         5:   "STATUS",
         24:  "<TERMTYPE>",
         25:  "<EOR>", 
         31:  "<NegoWindoSize>",
         32:  "<TERMSPEED>",
         34:  "<Linemode>",
         35:  "<XDISPLAY>",
         36:  "<ENV>",
         39:  "<NewENV>",
         85:  "COMPRESS (MCCP)",
         86:  "COMPRESS2 (MCCP)",
         91:  "MXP"}

# more info on 85/86/MCCP: http://www.randomly.org/projects/MCCP/protocol.html
# more info on 91/MXP: http://www.zuggsoft.com/zmud/mxp.htm

# telnet control codes
IAC  = chr(255)
DONT = chr(254)
DO   = chr(253)
WONT = chr(252)
WILL = chr(251)
SB   = chr(250)
GA   = chr(249)
NOP  = chr(241)
SE   = chr(240)
TELOPT_EOR = chr(239)
SEND = chr(1)
MODE = chr(1)
FORWARDMASK = chr(2)
IS   = chr(0)

# some nice strings to help with the telnet control code
# negotiation
DD       = DO + DONT
WW       = WILL + WONT
DDWW     = DD + WW

# telnet option codes
ECHO     = chr(1)
SGA      = chr(3)
TERMTYPE = chr(24)
EOR      = chr(25)
NAWS     = chr(31)
LINEMODE = chr(34)
ENV      = chr(39)

BELL     = chr(7)

def _fcc(code):
  if CODES.has_key(ord(code)):
    return CODES[ord(code)]
  return str(code)

def _cc(option):
  """
  Takes in an option string which we peel apart and return a pretty
  string representation of.

  @param option: the option string to convert
  @type  option: string

  @return: the string representation of the code
  @rtype: string
  """
  if len(option) == 3:
    return " ".join([_fcc(m) for m in option])

  return " ".join([_fcc(option[0]), _fcc(option[1]), _fcc(option[2]), 
                   option[3:-2], _fcc(option[-2]), _fcc(option[-1])])


class SocketCommunicator:
  """
  The SocketCommunicator handles all incoming and outgoing data from 
  and to the mud, telnet control codes, and some data transformations.
  """
  def __init__(self, e, ses, host, port):
    self._engine = e
    self._config = e.getConfigManager()

    self._sessionname = ''
    self._host = host
    self._port = port
    self._sock = None
    self._ansimode = 1
    self._nego_buffer = ''
    self._shutdownflag = 0
    self._session = ses

    self._debug = 0

    # this is the prompt regex that we use to split the incoming text.
    self._prompt_regex = self._buildPromptRegex()

    # this is the regex that we use to split the incoming text.
    delimiters = ( IAC+GA, IAC+TELOPT_EOR, "\n" )
    # make a regexp matching any of the delimiters above
    self._line_regex = re.compile("(" + "|".join(delimiters) + ")",
                                  re.MULTILINE | re.DOTALL)

    # "The server can do delimited prompts" flag
    self._good_prompts = 0

    # handle termtype issues
    if config.options.has_key("term"):
      self._termtype = config.options["term"][0]
    elif os.environ.has_key("TERM"):
      self._termtype = os.environ["TERM"]
    else:
      self._termtype = "lyntin"

    # we keep track of all the telnet stuff we're doing here
    # so we can look at it and dump it or whatever
    self._controllog = []

  def _buildPromptRegex(self, prompt=""):
    """
    Builds the prompt regex.  A prompt is IAC+GA or IAC+TELOPT_EOR or
    any string prompt.  Note that prompts eat up the characters.  So if
    the prompt is ">> " those characters will disappear from the stream.

    Note: the prompt is NOT escaped before it is added to the regexp.  It's
    up to you to re.escape the bits that need escaping.

    @param prompt: the text prompt to use (if any)
    @type  prompt: string

    @returns: the compiled regular expression for prompt detection
    @rtype: Regexp object
    """
    if prompt:
      r = "(" + IAC+GA + "|" + IAC+TELOPT_EOR + "|" + prompt + ")"
    else:
      r = "(" + IAC+GA + "|" + IAC+TELOPT_EOR + ")"
    return re.compile(r)

  def __repr__(self):
    return "connection %s %d" % (self._host, self._port)

  def logControl(self, str):
    self._controllog.append(str)

  def setSessionName(self, name):
    """
    Sets the session name.

    @param name: the new session name
    @type  name: string
    """
    self._sessionname = name

  def shutdown(self):
    """
    Shuts down the thread polling the socket connection and the socket
    as well.
    """
    self._shutdownflag = 1

  def connect(self, host, port, sessionname):
    """
    Takes in a host and a port and connects the socket.

    @param host: the host to connect to
    @type  host: string

    @param port: the port to connect at
    @type  port: int

    @param sessionname: the name of the new session
    @type  sessionname: string
    """
    if not self._sock:
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      sock.connect((host, port))
      sock.setblocking(1)

      self._host = host
      self._port = port
      self._sock = sock
      self._sessionname = sessionname

      ses = exported.get_session(sessionname)

      exported.hook_spam("connect_hook", \
              {"session": ses, "host": host, "port": port})
    else:
      raise Exception("Connection already exists.")


  def _pollForData(self):
    """
    Polls the socket for data.
    """
    readers, e, w = select.select([self._sock], [], [], .2)
    if readers:
      return readers[0].recv(1024)

    return None

  def _filterIncomingData(self, data):
    """
    run the data through the net_read_data_filter hook which
    allows things like compressors and other data transformation
    mechanisms to do their thing.
    """
    # be careful--this catches both the '' and the None situations
    if not data:
      return data

    spamargs = {"session": self._session, "data": data, "dataadj": data}
    spamargs = exported.hook_spam("net_read_data_filter", 
          argmap=spamargs, mappingfunc=exported.filter_mapper)

    if spamargs == None:
      data = ""
    else:
      data = spamargs["dataadj"]

    return data

  def run(self):
    """
    While the connection hasn't been shut down, we spin through this
    loop retrieving data from the mud, 
    """
    from lyntin import exported
    try:
      data = ''
      while not self._shutdownflag:
        newdata = self._pollForData()

        if newdata:
          newdata = self._filterIncomingData(newdata)
          if newdata == "":
            continue

          last_index = 0
          alldata = (data+newdata).replace("\r","")
          # incrementally walk through each line in the data,
          # adjusting last_index to the end of the previous match
          for (m) in self._line_regex.finditer(alldata):
            oneline = alldata[last_index:m.end()]
            last_index = m.end()
            self.handleData(oneline)
          # keep the remainder (empty if alldata ended with a delimiter)
          data = alldata[last_index:]

        elif newdata == '':
          # if we got back an empty string, then something's amiss
          # and we should dump them.
          if data:
            self.handleData(data)
          if self._shutdownflag == 0 and self._session:
            self._session.shutdown(())
          break

        elif not self._good_prompts and data:
          # Now we have rest of the input which is neither delimited prompt
          # nor complete line, and we yet did not see this server 
          # delimiting it's prompts with telnet GA or EOR option.
          # We'll handle these data because the socket read was timed out.
          self.handleData(data)
          data = ''

    except SystemExit:
      if self._session:
        self._session.shutdown(())

    except:
      exported.write_traceback("socket exception")
      if self._session:
        self._session.shutdown(())

    # if we hit this point, we want to shut down the socket
    try:    self._sock.shutdown(2)
    except: pass

    try:    self._sock.close()
    except: pass

    self._sock = None
    self._session = None

    # sometimes the mud will hose up with echo off--we want to kick it
    # on again.
    self._config.change("mudecho", "on")

    # output message so the user knows what happened.
    event.OutputEvent(message.Message("Lost connection to: %s\n" % self._host)).enqueue()

  def write(self, data, convert=1):
    """
    Writes data to the mud after passing it through net_write_data_filter.

    @param data: the data to write to the socket
    @type  data: string

    @param convert: whether (1) or not (0) we should convert eol stuff to 
        CRLF and IAC to IAC IAC.
    @type  convert: boolean

    @raises Exception: if we have problems sending the data over the
        socket
    """
    if convert:
      data = data.replace("\n", "\r\n")

      if IAC in data:
        data = data.replace(IAC, IAC+IAC)

    if self._shutdownflag == 0:
      # run the data through the net_write_data_filter hook which
      # allows things like compressors and other data transformation
      # mechanisms to do their thing.
      spamargs = {"session": self._session, "data": data, "dataadj": data}
      spamargs = exported.hook_spam("net_write_data_filter", argmap=spamargs, 
            mappingfunc=exported.filter_mapper)

      if spamargs == None:
        return
      else:
        data = spamargs["dataadj"]
 
      try:
        self._sock.send(data)

      except Exception, e:
        if self._shutdownflag == 0 and self._session:
          self._session.shutdown(())
          raise Exception(e)

      return None

  def handleData(self, data):
    """
    Handles incoming data from the mud.  We wrap it in a MudEvent
    and toss it on the queue.

    @param data: the incoming data from the mud
    @type  data: string
    """
    global BELL

    # handle the bell
    count = data.count(BELL)
    for i in range(count):
      event.SpamEvent(hookname="bell_hook", argmap={"session": self._session}).enqueue()
    data = data.replace(BELL, "")

    # handle telnet option stuff
    if IAC in data:
      data = self.handleNego(data)

    if not self._config.get("promptdetection") or data.endswith("\n"):
      event.MudEvent(self._session, data).enqueue() 
    else:
      event.SpamEvent(hookname="prompt_hook", argmap={"session": self._session, "prompt": data}).enqueue()


  def handleNego(self, data):
    """
    Removes telnet negotiation stuff from the stream and handles it.

    @param data: the incoming data from the mud that we need to parse
        for telnet control code stuff
    @type  data: string

    @return: the data without the telnet control codes
    @rtype:  string
    """
    marker = -1
    i = data.find(IAC)

    while (i != -1):
      if i + 1 >= len(data):
        marker = i
        break

      if data[i+1] == NOP:
        data = data[:i] + data[i+2:]
        self.logControl("receive: IAC NOP")

      elif data[i+1] == GA or data[i+1] == TELOPT_EOR:
        data = data[:i] + data[i+2:]
        # if data is a prompt delimited with some telnet option, 
        # then we'll mark the server as "server with good prompting" 
        self._good_prompts = 1

      elif data[i+1] == IAC:
        data = data[:i] + data[i+1:]
        i = i + 1
        self.logControl("receive: IAC IAC")

      else:
        if i + 2 >= len(data):
          marker = i
          break

        # handles DO/DONT/WILL/WONT stuff
        if data[i+1] in DDWW:
          option = data[i:i+3]

          self.logControl("receive: " + _cc(option))
          if option[2] == ECHO:
            if option[1] == WILL:
              self._config.change("mudecho", "off")
            elif option[1] == WONT:
              self._config.change("mudecho", "on")

          elif option[2] == TERMTYPE:
            if option[1] == DO:
              self.write(IAC + WILL + TERMTYPE, 0)
              self.logControl("send: IAC WILL TERMTYPE")
            else:
              self.write(IAC + WONT + TERMTYPE, 0)
              self.logControl("send: IAC WONT TERMTYPE")

          elif option[2] == EOR:
            if option[1] == WILL:
              self.write(IAC + DO + EOR, 0)
              self.logControl("send: IAC DO EOR")

          else:
            args = {"session": self._session, "data": option}
            # this will give us back the args (in the case that no one
            # handled it) or None (in the case that someone handled it
            # and raised a StopSpammingException).
            ret = exported.hook_spam("net_handle_telnet_option", args)

            if ret:
              if option[1] in DD:
                self.write(IAC + WONT + option[2], 0)
                self.logControl("send: " + _cc(IAC + WONT + option[2]))

              elif option[1] in WW:
                self.write(IAC + DONT + option[2], 0)
                self.logControl("send: " + _cc(IAC + DONT + option[2]))

          data = data[:i] + data[i+3:]

        # handles SB...SE stuff
        elif data[i+1] == SB:

          end = data.find(SE, i)
          if end == -1:
            marker = i
            break

          option = data[i:end+1]
          self.logControl("receive: " + _cc(option))

          if option[2] == TERMTYPE and option[3] == SEND:
            self.write(IAC + SB + TERMTYPE + IS + self._termtype + IAC + SE, 0)
            self.logControl("send: IAC SB TERMTYPE IS " + self._termtype + " IAC SE")
          else:
            args = {"session": self._session, "data": option}
            # this will give us back the args (in the case that no one
            # handled it) or None (in the case that someone handled it
            # and raised a StopSpammingException).
            ret = exported.hook_spam("net_handle_telnet_option", args)

          data = data[:i] + data[end+1:]

        # in case they passed us something weird we remove the IAC and 
        # move on
        else:
          data = data[:i] + data[i+1:]

      i = data.find(IAC, i)

    if marker != -1:
      self._nego_buffer = data[marker:]
      data = data[:marker]

    return data

# Local variables:
# mode:python
# py-indent-offset:2
# tab-width:2
# End:
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.