#MUDPyE - (M)ulti-(U)ser (D)imension (Py)thon (E)ngine
#Copyright (C) 2005  Corey Staten

#This program 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 2
#of the License, or (at your option) any later version.

#This program 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, write to the Free Software
#Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

#Send feedback/questions to MUDPyE@gmail.com

import select
import socket
import thread
import time

from mpmudobject import Ex

class DNSThread(object):
  """Object that creates a DNS Resolution thread on startup."""

  def __init__(self):
    self.lock = thread.allocate_lock()
    self.requests = []
    self.shutdown = False
    self.threadID = 0
    threadID = thread.start_new_thread(self.__Thread_DNSResolve, ())

  def __del__(self):
    #Inform the DNS resolution thread to shut down.
    self.lock.acquire()
    self.shutdown = True
    self.lock.release()

  def AddRequest(self, client, ipAddr):
    self.lock.acquire()
    self.requests.append((client,ipAddr))
    self.lock.release()

  def __Thread_DNSResolve(self):
    while True:
      self.lock.acquire()
      if self.shutdown == True:
        break
      curRequests = self.requests
      self.requests = []
      self.lock.release()
      for address in curRequests:
        ipAddr = address[1]
        hostAddr = socket.getfqdn(ipAddr)
        address[0].DNSResolved(hostAddr)
      time.sleep(.2)

class TelnetClient(object):
  """This client implements the telnet protocol in a way suitable for MUDs.
     Telnet commands are handled out-of-stream by this class and passed along to the MUD separately
     from user commands."""
  STATE_TEXT, STATE_IAC, STATE_DO, STATE_DONT, STATE_WILL, STATE_WONT, STATE_SUBREQUEST, STATE_SUBREQUESTIAC = range(8)
  def __init__(self, server, clientSocket, connAddress):
    self.server = server
    self.clientSocket = clientSocket
    self.ipAddr = connAddress[0]
    self.hostAddr = ""
    self.portNum = connAddress[1]
    self.avatarID = ""
    self.sendBuf = []
    self.recvBuf = []
    self.subBuf = []
    self.state = TelnetClient.STATE_TEXT
    self.clientSocket.setblocking(0)
    self.linkdead = False

  def Disconnect(self):
    """Disconnects the client.  Shortcut to server.DisconnectClient(self)"""
    self.server.DisconnectClient(self)

  def _Linkdeath(self):
    """Handles alerting the MUD that this client has gone linkdead."""
    self.linkdead = True
    Ex(self.server.eventObj, "_Sys_ClientLinkdeath", self)
    self.server.DisconnectClient(self)

  def Send(self, message):
    """Adds message to a list of messages to be sent.  These are joined when necessary in SockSend."""
    self.sendBuf.append(message)

  def GetMessage(self):
    """Used to cut a message from in front of a newline in standard mud fashion."""
    for char in self.recvBuf:
      if (char == '\n') or (char == '\r'):
        pos = self.recvBuf.index(char)
        message = self.recvBuf[:pos].strip()
        #Remove any extra \n or \r characters.
        self.recvBuf = self.recvBuf[(pos+1):].lstrip()
        return message
    return None

  def GetRaw(self):
    """Returns and clears the raw recv buffer."""
    returnString = self.recvBuf
    self.recvBuf = ""
    return returnString

  def SockSend(self):
    """Handles joining the buffer and sending the text."""
    try:
      sendText = "".join(self.sendBuf)
      self.sendBuf = []
      while len(sendText):
        bytes = self.clientSocket.send(sendText)
        sendText = sendText[bytes:]
    except socket.error:
      if not self.linkdead:
        self._LinkDeath()
      

  def SockRecv(self):
    """Handles actual receipt of data, including maintenance of the telnet state machine."""
    try:
      recvMessage = self.clientSocket.recv(4096)
    except socket.error:
      self._Linkdeath()
      return None
    if recvMessage == "":
      #Client disconnected of his own accord.
      self._Linkdeath()
      return None
    commands = []
    inNewLine = False
    #Change
    #Telnet state machine.
    for char in recvMessage:
      if self.state == TelnetClient.STATE_TEXT:
        #IAC
        if char == "\xFF":
          inNewLine = False
          self.state = TelnetClient.STATE_IAC
        elif char == "\b":
          inNewLine = False
          if len(self.recvBuf):
            del self.recvBuf[-1]
        #Valid endlines include \r\n, \n\r, \r\0, etc.
        elif (char == "\n") or (char == "\r") or (char == "\0"):
          if not(inNewLine):
            commands.append("".join(self.recvBuf))
            inNewLine = True
            self.recvBuf = []
        else:
          inNewLine = False
          self.recvBuf.append(char)
      elif self.state == TelnetClient.STATE_IAC:
        #IAC
        if char == "\xFF":
          self.state = TelnetClient.STATE_TEXT
          self.recvBuf.append(char)
        #DONT
        elif char == "\xFE":
          self.state = TelnetClient.STATE_DONT
        #DO
        elif char == "\xFD":
          self.state = TelnetClient.STATE_DO
        #WONT
        elif char == "\xFC":
          self.state = TelnetClient.STATE_WONT
        #WILL
        elif char == "\xFB":
          self.state = TelnetClient.STATE_WILL
        #SB
        elif char == "\xFA":
          self.state = TelnetClient.STATE_SUBREQUEST
      elif self.state == TelnetClient.STATE_DONT:
        Ex(self.avatarObj, "_Sys_TelnetDONT", char)
        self.state = TelnetClient.STATE_TEXT
      elif self.state == TelnetClient.STATE_DO:
        Ex(self.avatarObj, "_Sys_TelnetDO", char)
        self.state = TelnetClient.STATE_TEXT
      elif self.state == TelnetClient.STATE_WONT:
        Ex(self.avatarObj, "_Sys_TelnetWONT", char)
        self.state = TelnetClient.STATE_TEXT
      elif self.state == TelnetClient.STATE_WILL:
        Ex(self.avatarObj, "_Sys_TelnetWILL", char)
        self.state = TelnetClient.STATE_TEXT
      elif self.state == TelnetClient.STATE_SUBREQUEST:
        #IAC
        if char == "\xFF":
          self.state = TelnetClient.STATE_SUBREQUESTIAC
        else:
          self.subBuf.append(char)
      elif self.state == TelnetClient.STATE_SUBREQUESTIAC:
        #IAC
        if char == "\xFF":
          self.subBuf.append(char)
          self.state = TelnetClient.STATE_SUBREQUEST
        #SE
        elif char == "\xF0":
          self.state = TelnetClient.STATE_TEXT
          recvString = "".join(self.subBuf)
          self.subBuf = []
          Ex(self.avatarObj, "_Sys_TelnetSB", recvString)
    for command in commands:
      Ex(self.avatarObj, "_Sys_ReceiveCommand", command)

  def DNSResolved(self, hostAddr):
    self.hostAddr = hostAddr
    self.server.mudWorld.loggers["engine.server"].info("DNS Resolved [%s : %s]" % (self.ipAddr, self.hostAddr))
    Ex(self.avatarObj, "_Sys_DNSResolved", hostAddr)

class MudServer(object):
  def __init__(self, mudWorld, portNum, maxConn, eventObj, clientAvatarType, clientClass):
    self.mudWorld = mudWorld
    self.mudWorld.servers.append(self)
    self.clientAvatarType = clientAvatarType
    self.clientClass = clientClass
    self.portNum = portNum
    self.maxConn = maxConn
    self.eventObj = eventObj
    self.clientList = []
    self.hostAddr = socket.gethostname()
    self.ipAddr = socket.gethostbyname(self.hostAddr)
    self.serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    self.serverSocket.bind(("", self.portNum))
    self.serverSocket.listen(10)
    self.mudWorld.loggers["engine.server"].info("Server listening on port [%d] [%s : %s]" % (self.portNum, self.ipAddr, self.hostAddr))

  def AcceptConnections(self):
    while 1:
      if len(self.clientList) > self.maxConn:
        break
      acceptList, unusedWrite, unusedErr = select.select([self.serverSocket], [], [], 0)
      del unusedWrite
      del unusedErr
      if acceptList == []:
        break
      else:
        (newConn, connAddress) = self.serverSocket.accept()
        newClient = self.clientClass(self, newConn, connAddress)
        self.mudWorld.loggers["engine.server"].info("Client [%s] connected." % newClient.ipAddr)
        newAvatarObj = self.mudWorld.objDB.CreateObj(self.clientAvatarType, client=newClient)
        newClient.avatarObj = newAvatarObj
        self.clientList.append(newClient)
        self.mudWorld.dns.AddRequest(newClient, newClient.ipAddr)
        Ex(self.eventObj, "_Sys_ClientConnect", newClient)

  def Shutdown(self):
    self.DisconnectAllClients()
    self.serverSocket.close()
    self.mudWorld.loggers["engine.server"].info("Shutting down server on port [%d] [%s : %s]" % (self.portNum, self.ipAddr, self.hostAddr))
    self.mudWorld.servers.remove(self)

  def DisconnectAllClients(self):
    for client in self.clientList:
      self.DisconnectClient(client)

  def DisconnectClient(self, discClient):
    if not(discClient.linkdead):
      Ex(self.eventObj, "_Sys_ClientDisconnect", discClient)
      discClient.SockSend()
    try:
      discClient.clientSocket.shutdown(2)
    except socket.error:
      pass
    try:
      discClient.clientSocket.close()
    except socket.error:
      pass
    if not discClient.linkdead:
      self.mudWorld.loggers["engine.server"].info("Client [%s:%s] disconnected." % (discClient.ipAddr, discClient.hostAddr))
    else:
      self.mudWorld.loggers["engine.server"].info("Client [%s:%s] linkdeath." % (discClient.ipAddr, discClient.hostAddr))
    self.clientList.remove(discClient)

  def NetworkUpdate(self):
    if len(self.clientList) == 0:
      return None
    clientSockList = [client.clientSocket for client in self.clientList]
    #First, check for errors.
    unusedRead, unusedWrite, errSockets = select.select([], [], clientSockList, 0)
    errClients = [client for client in self.clientList if (client.clientSocket in errSockets)]
    for client in errClients:
      self.DisconnectClient(client)
    #Next, check for readable sockets.
    readSockets, unusedWrite, unusedErr = select.select(clientSockList, [], [], 0)
    readClients = [client for client in self.clientList if (client.clientSocket in readSockets)]
    for client in readClients:
      client.SockRecv()
    #Don't even bother checking for  sendable sockets, if they exist, we'll assume they're sendable.
    for client in self.clientList:
      client.SockSend()