#----------------------------------------------------------------------
#	poosock.py				JJS 05/20/99
#
#	This file defines a main program which forms a POO server
#	using TCP/IP sockets.
#
#----------------------------------------------------------------------

# standard modules
import whrandom
import string
import types
import copy
import sys
import md5
from socket import *
from select import *	# if you don't use this, change useSelect below
from time import sleep, ctime, time

# POO modules
import poo
poo.flushNoCR = 1

#----------------------------------------------------------------------
# global (module) vars
#
HOST = ''			# null string means local host (this machine)
PORT = 4000			# port to use
MAXCONS = 10		# maximum connections to allow
SHUTDOWN = "shutdown"	# shutdown command
SAVE = "savedb"		# save command
TIMEOUT = 0.33		# max time to wait between poo.gUpdate() calls
Banned = []			# list of domains/hosts not allowed to log on
Allowed = []		# list of domains/hosts allowed to log on
useSelect = 1		# set to 1 to use select(), 0 otherwise

endl = "\r\n"		# string to end outgoing lines (for standard Telnet)
running = 0			# 0=starting up, 1=running, -1=shutting down
sock = None			# socket used to receive connections
connlist = []		# list of connected users
LoginList = []		# list of people trying to log in
stdout = sys.stdout	# standard output (server console)

#----------------------------------------------------------------------
# function to print to the log file (stdout)
#
def Log(msg, addr=None):
	sys.stdout = stdout
	if not addr: print "%-43s" % ctime(time())[:19], msg
	else: print "%19s  %15s %6d" %  \
	  (ctime(time())[:19], addr[0], addr[1]), msg
	stdout.flush()

#----------------------------------------------------------------------
# function to find the end of line character(s)
#	return '\n', '\r', '\r\n', etc., or '' if none of these found
#
def EolChars(s):
	possibilities = ('\r\n', '\n\r', '\n', '\r')
	for p in possibilities:
		if string.rfind(s, p) >= 0:
			Log("EOL: " + repr(p))
			return p
	return ''

#----------------------------------------------------------------------
# function to negotiate TELNET settings
#	(for more info, see ftp://ftp.internic.net/rfc/rfc854.txt)
#	...strip negotiation from the data, and return any remaining data
#
def Negotiate(data, conn):
	# If we get a \377, then some special telnet code is next.
	# codes \373--\376 are followed by a third byte specifying the 'option code'
	# other codes are just two bytes
	while '\377' in data:
		x=string.find(data, '\377')
		if len(data) > x+2 and data[x+1] in '\373\374\375\376': 
			if data[x+1] in '\375\376':
				# It's a DO or a DON'T
				# and we WON'T
				conn.write('\377\374'+data[x+2])
			data=data[:x]+data[x+3:]
		else:
			data=data[:x]+data[x+2:]
	return data

#----------------------------------------------------------------------
# function to see if host/domain is allowed to connect
# NOTE: if the host is not allowed OR banned, the host can't connect
#	Returns 1 if host is banned, 0 if allowed
#
def CheckDomain(addr):
        out = 0
        # See if they are allowed
        ad = string.splitfields(addr[0],'.')
        for dh in Allowed:
                print "Checking "+dh[0]+"."+dh[1]+"."+dh[2]+"."+dh[3]+"."
                if dh[0] == '*': ad[0] = '*'
                if dh[1] == '*': ad[1] = '*'
                if dh[2] == '*': ad[2] = '*'
                if dh[3] == '*': ad[3] = '*'
                if ad == dh and out == 0:
                        out = 0
                        print " Allowed",ad[0]+"."+ad[1]+"."+ad[2]+"."+ad[3]+"."
                elif not ad == dh:
                        out = 1
                        print "Not Allowed",ad[0]+"."+ad[1]+"."+ad[2]+"."+ad[3]+"."

        # See if they are banned
        ad = string.splitfields(addr[0],'.')
        for dh in Banned:
                print "Checking "+dh[0]+"."+dh[1]+"."+dh[2]+"."+dh[3]+"."
                if dh[0] == '*': ad[0] = '*'
                if dh[1] == '*': ad[1] = '*'
                if dh[2] == '*': ad[2] = '*'
                if dh[3] == '*': ad[3] = '*'
                if not ad == dh and out == 0:
                        out = 0
                        print "Not Banned",ad[0]+"."+ad[1]+"."+ad[2]+"."+ad[3]+"."
                elif ad == dh:
                        out = 1
                        print "Banned",ad[0]+"."+ad[1]+"."+ad[2]+"."+ad[3]+"."

        return out

#----------------------------------------------------------------------
# function to start the server
#
def StartServer():
	global HOST, PORT, running, sock

	sock = socket(AF_INET, SOCK_STREAM)
	try: sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
	except: pass
	sock.bind(HOST, PORT)
	sock.listen(1)
	sock.setblocking(0)
	Log( "Server open on port " + str(PORT) )
	running = 1

#----------------------------------------------------------------------
# function to disconnect a user
#
def Disconnect(user, msg=''):
	global endl,connlist, LoginList

	Log("disconnected "+user.name, user.addr)
	if user.conn and msg: user.send( msg+endl )
	if user.POOuser:
		user.POOuser.Logout()
		try: user.POOuser.__dict__['__outstream'].close()
		except: pass
	if user.conn:
		user.conn.close()
		user.conn = None
	if user in connlist: connlist.remove(user)
	if user in LoginList: LoginList.remove(user)
	user.connected = 0

#----------------------------------------------------------------------
# function to shut the server down
#
def ShutDown():
	global connlist, running, endl, sock, stdout

	if running < -1: return
	Log( "Shutting server down." )
	for p in connlist: Disconnect(p, "Server shutting down.")
	poo.gSave()
	sys.stdout = stdout
	sock.close()
	running = -2

#----------------------------------------------------------------------
# handle a new connection
#
def NewConn( conn, addr ):
	global connlist, MAXCONS, endl, stdout

	Log("Connection Received", addr )
	if running < 1:
		conn.send("Server is not running." + endl)
		conn.close()
	if len(connlist)+len(LoginList) >= MAXCONS:
		conn.send("Sorry, only " + str(MAXCONS) + \
			" connections are allowed.  Please try again later." \
			+ endl )
		conn.close()
		return
	if CheckDomain(addr):
		conn.send("Sorry, connections from your host/domain " + \
			"are not allowed at this site." + endl )
		conn.close()
	# looks good -- log 'em in...
	Conn(conn, addr)

#----------------------------------------------------------------------
# network update -- call this function periodically
#
def NetUpdate():
	global connlist, running, sock

	if not running: return StartServer()
	if running < 0: return ShutDown()
	
	# check for incoming connections
	try:
		conn, addr = sock.accept()
		NewConn( conn, addr )
	except: pass

	# handle incoming messages
	connsToCheck = connlist + LoginList
	sys.stdout = stdout
	if useSelect:
		filenums = map(lambda x:x.fileno(), connsToCheck)
		fnswinput = select(filenums, [], [], TIMEOUT)[0]
		connsToCheck = filter(lambda x,y=fnswinput:x.fileno() in y,
					connsToCheck)
	for u in connsToCheck:
		try:
			data = u.conn.recv(1024)
		except: data = None
		if data:
			if '\377' in data: data = Negotiate(data, u)
			#data = filter(lambda x: x>=' ' or x=='\n' or x=='\t', data)
			stripped = string.rstrip(data)
			if data: u.HandleMsg(data)
		elif data == '':
			# if they were in the recv(), but data is '', they have disconnected
			Disconnect(u,"disconnected")

#----------------------------------------------------------------------
#
# Conn class -- a connection, its POO object, etc.
#
class Conn:

	def __init__(self,conn, addr=''):
		global LoginList
		self.connected = 0
		self.conn = conn
		self.addr = addr
		self.POOuser = None
		self.name = '<login>'
		self.password = ''
		self.tries = 0
		self.partialtext = ''
		self.eolchars = ''
		LoginList.append(self)
		if self.conn: self.conn.setblocking(0)

	    # show login message
		try:
			f = open('connect.txt','r')
			for line in f.readlines(): self.send(line[:-1]+endl)
		except: pass

		self.send("Name: ")

	def send(self, pWhat):
		if not self.conn: return
		try: self.conn.send(pWhat)
		except: pass
	write = send
	
	def flush(self):
		try: self.conn.flush()
		except: pass

	def HandleMsg(self,msg):
		global running
		
		# split msg into lines, 
		# passing complete lines to the POO engine,
		# and caching incomplete lines for future use
		if not self.eolchars:
			self.eolchars = EolChars(msg)
			if not self.eolchars:
				self.partialtext = self.partialtext + msg
				return

		# split into lines...
		eollen = len(self.eolchars)
		if self.partialtext:
			msg = self.partialtext + msg
			self.partialtext = ''
		while msg:
			eol = string.find( msg, self.eolchars )
			if eol >= 0:
				line = msg[:eol]
				msg = msg[eol + eollen:]

				# check for special poosock messages
				if line == SHUTDOWN:
					Log("Got Shutdown Command", self.addr)
					running=-1
				elif line == SAVE: poo.gSave()
				elif line == "quit": Disconnect(self,"Goodbye!")
				else:
					# do normal POO message handling
					if self.POOuser: self.POOuser.handleMsg( line )
					else: self.HandleLoginLine( line )

			else:
				# incomplete line -- save for later
				self.partialtext = self.partialtext + msg
				return

	def HandleLoginLine(self, msg):
		if self.name == '<login>':	# first msg should be name
			if len(msg)<2: return
			words = map(lambda x:string.strip(x), string.split(msg))
			if (words[0] == "connect" or words[0] == "c") and len(words) > 2:
				self.HandleLoginLine(words[1])
				self.HandleLoginLine(words[2])
				return
			self.name = msg
			if msg == 'guest':
				self.HandleLoginLine('guest')	# auto-password
			else:
				self.send('Password: ')
			return
		if self.password == '':
			self.password = md5.new(msg).digest()
			# check to see if this matches some player
			mtch = filter(self.Matches, poo.gObjlist.values())
			if not mtch:
				self.tries = self.tries + 1
				if self.tries >= 3:
					return self.Abort('Invalid login.');
				self.name = '<login>'
				self.password = ''
				self.send('Login incorrect.' + endl + 'Name: ')
				return				
			mtch = mtch[0]	# shouldn't be more than one!
			if mtch.connected():
				# user is already logged in...
				#return self.Abort('Already logged in!');
				self.send('NOTE: previous connection has been aborted.' + endl)
			# looks good -- let 'em in
			self.EnterPOO(mtch)

	def Abort(self,msg='You are kicked off:'):
		global endl, LoginList, connlist
		if msg: self.send(msg+endl)
		Log('Aborting ['+msg+']', self.addr)
		self.conn.close()
		# we should be in LoginList; but we'll check both to be sure:
		if self in LoginList: LoginList.remove(self)
		if self in connlist: connlist.remove(self)

	def fileno(self):
		try: return self.conn.fileno()
		except: return None

	def Matches(self,puser):	# return 1 if matches this POOuser
		try:
			return (self.password == puser.password and \
				self.name == puser.name)
		except: return 0

	def EnterPOO(self,match):
		global LoginList, connlist, endl
		# make sure if any previous connections match the same player,
		# we disconnect them now
		for c in connlist:
			if c.POOuser == match: Disconnect(c, "reconnection from %s %s" % tuple(self.addr))
		self.POOuser = match
		LoginList.remove(self)
		connlist.append(self)
		self.POOuser.Login( poo.Outfix(self, self.POOuser ))
		Log(self.name+" logged in", self.addr)

# end of class Conn

#----------------------------------------------------------------------

#### MAIN PROGRAM ####

Log( "Loading database")
poo.initialize()				# load file

# here set Banned and Allowed domains, e.g.:
# Allowed = [['192','101','199','*']]

Log("Starting server")
StartServer()				# start the server

while running > -2:
	if not useSelect: sleep(TIMEOUT)
	NetUpdate()	# loop until done
	poo.gUpdate()

Log("Program terminated.")