aboutsummaryrefslogtreecommitdiffstats
path: root/src/telnetsrvlib.py
blob: 14b9b2f9b99c7c4ef8c07fad2d34c7b7075c8985 (plain) generated by cgit v1.2.3 (git 2.25.1) at 2024-12-25 06:15:50 +0000 a id='n175' href='#n175'>175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
#!/usr/bin/python
"""TELNET server class

Based on the telnet client in telnetlib.py

Presents a command line interface to the telnet client.
Various settings can affect the operation of the server:

	authCallback = Reference to authentication function. If
                   there is none, no un/pw is requested. Should
                   raise an exception if authentication fails
                   Default: None
	authNeedUser = Should a username be requested?
                   Default: False
	authNeedPass = Should a password be requested?
                   Default: False
	COMMANDS     = Dictionary of supported commands
                   Key = command (Must be upper case)
                   Value = List of (function, help text)
                   Function.__doc__ should be long help
				   Function.aliases may be a list of alternative spellings
"""

#from telnetlib import IAC, WILL, WONT, DO, DONT, ECHO, SGA, Telnet
import threading
import SocketServer
import socket
import time
import sys
import traceback
import curses.ascii
import curses.has_key
import curses
import logging
import re
if not hasattr(socket, 'SHUT_RDWR'):
	socket.SHUT_RDWR = 2

__all__ = ["TelnetHandler", "TelnetCLIHandler"]

IAC  = chr(255) # "Interpret As Command"
DONT = chr(254)
DO   = chr(253)
WONT = chr(252)
WILL = chr(251)
theNULL = chr(0)

SE  = chr(240)  # Subnegotiation End
NOP = chr(241)  # No Operation
DM  = chr(242)  # Data Mark
BRK = chr(243)  # Break
IP  = chr(244)  # Interrupt process
AO  = chr(245)  # Abort output
AYT = chr(246)  # Are You There
EC  = chr(247)  # Erase Character
EL  = chr(248)  # Erase Line
GA  = chr(249)  # Go Ahead
SB =  chr(250)  # Subnegotiation Begin

BINARY = chr(0) # 8-bit data path
ECHO = chr(1) # echo
RCP = chr(2) # prepare to reconnect
SGA = chr(3) # suppress go ahead
NAMS = chr(4) # approximate message size
STATUS = chr(5) # give status
TM = chr(6) # timing mark
RCTE = chr(7) # remote controlled transmission and echo
NAOL = chr(8) # negotiate about output line width
NAOP = chr(9) # negotiate about output page size
NAOCRD = chr(10) # negotiate about CR disposition
NAOHTS = chr(11) # negotiate about horizontal tabstops
NAOHTD = chr(12) # negotiate about horizontal tab disposition
NAOFFD = chr(13) # negotiate about formfeed disposition
NAOVTS = chr(14) # negotiate about vertical tab stops
NAOVTD = chr(15) # negotiate about vertical tab disposition
NAOLFD = chr(16) # negotiate about output LF disposition
XASCII = chr(17) # extended ascii character set
LOGOUT = chr(18) # force logout
BM = chr(19) # byte macro
DET = chr(20) # data entry terminal
SUPDUP = chr(21) # supdup protocol
SUPDUPOUTPUT = chr(22) # supdup output
SNDLOC = chr(23) # send location
TTYPE = chr(24) # terminal type
EOR = chr(25) # end or record
TUID = chr(26) # TACACS user identification
OUTMRK = chr(27) # output marking
TTYLOC = chr(28) # terminal location number
VT3270REGIME = chr(29) # 3270 regime
X3PAD = chr(30) # X.3 PAD
NAWS = chr(31) # window size
TSPEED = chr(32) # terminal speed
LFLOW = chr(33) # remote flow control
LINEMODE = chr(34) # Linemode option
XDISPLOC = chr(35) # X Display Location
OLD_ENVIRON = chr(36) # Old - Environment variables
AUTHENTICATION = chr(37) # Authenticate
ENCRYPT = chr(38) # Encryption option
NEW_ENVIRON = chr(39) # New - Environment variables
# the following ones come from
# http://www.iana.org/assignments/telnet-options
# Unfortunately, that document does not assign identifiers
# to all of them, so we are making them up
TN3270E = chr(40) # TN3270E
XAUTH = chr(41) # XAUTH
CHARSET = chr(42) # CHARSET
RSP = chr(43) # Telnet Remote Serial Port
COM_PORT_OPTION = chr(44) # Com Port Control Option
SUPPRESS_LOCAL_ECHO = chr(45) # Telnet Suppress Local Echo
TLS = chr(46) # Telnet Start TLS
KERMIT = chr(47) # KERMIT
SEND_URL = chr(48) # SEND-URL
FORWARD_X = chr(49) # FORWARD_X
PRAGMA_LOGON = chr(138) # TELOPT PRAGMA LOGON
SSPI_LOGON = chr(139) # TELOPT SSPI LOGON
PRAGMA_HEARTBEAT = chr(140) # TELOPT PRAGMA HEARTBEAT
EXOPL = chr(255) # Extended-Options-List
NOOPT = chr(0)

#Codes used in SB SE data stream for terminal type negotiation
IS = chr(0)
SEND = chr(1)

CMDS = {
	WILL: 'WILL',
	WONT: 'WONT',
	DO: 'DO',
	DONT: 'DONT',
	SE: 'Subnegotiation End',
	NOP: 'No Operation',
	DM: 'Data Mark',
	BRK: 'Break',
	IP: 'Interrupt process',
	AO: 'Abort output',
	AYT: 'Are You There',
	EC: 'Erase Character',
	EL: 'Erase Line',
	GA: 'Go Ahead',
	SB: 'Subnegotiation Begin',
	BINARY: 'Binary',
	ECHO: 'Echo',
	RCP: 'Prepare to reconnect',
	SGA: 'Suppress Go-Ahead',
	NAMS: 'Approximate message size',
	STATUS: 'Give status',
	TM: 'Timing mark',
	RCTE: 'Remote controlled transmission and echo',
	NAOL: 'Negotiate about output line width',
	NAOP: 'Negotiate about output page size',
	NAOCRD: 'Negotiate about CR disposition',
	NAOHTS: 'Negotiate about horizontal tabstops',
	NAOHTD: 'Negotiate about horizontal tab disposition',
	NAOFFD: 'Negotiate about formfeed disposition',
	NAOVTS: 'Negotiate about vertical tab stops',
	NAOVTD: 'Negotiate about vertical tab disposition',
	NAOLFD: 'Negotiate about output LF disposition',
	XASCII: 'Extended ascii character set',
	LOGOUT: 'Force logout',
	BM: 'Byte macro',
	DET: 'Data entry terminal',
	SUPDUP: 'Supdup protocol',
	SUPDUPOUTPUT: 'Supdup output',
	SNDLOC: 'Send location',
	TTYPE: 'Terminal type',
	EOR: 'End or record',
	TUID: 'TACACS user identification',
	OUTMRK: 'Output marking',
	TTYLOC: 'Terminal location number',
	VT3270REGIME: '3270 regime',
	X3PAD: 'X.3 PAD',
	NAWS: 'Window size',
	TSPEED: 'Terminal speed',
	LFLOW: 'Remote flow control',
	LINEMODE: 'Linemode option',
	XDISPLOC: 'X Display Location',
	OLD_ENVIRON: 'Old - Environment variables',
	AUTHENTICATION: 'Authenticate',
	ENCRYPT: 'Encryption option',
	NEW_ENVIRON: 'New - Environment variables',
}

class TelnetHandler(SocketServer.BaseRequestHandler):
	"A telnet server based on the client in telnetlib"

	# What I am prepared to do?
	DOACK = {
		ECHO: WILL,
		SGA: WILL,
		NEW_ENVIRON: WONT,
	}
	# What do I want the client to do?
	WILLACK = {
		ECHO: DONT,
		SGA: DO,
		NAWS: DONT,
		TTYPE: DO,
		LINEMODE: DONT,
		NEW_ENVIRON: DO,
	}
	# Default terminal type - used if client doesn't tell us its termtype
	TERM = "ansi"
	# Keycode to name mapping - used to decide which keys to query
	KEYS = {					# Key escape sequences
		curses.KEY_UP: 'Up',			# Cursor up
		curses.KEY_DOWN: 'Down',		# Cursor down
		curses.KEY_LEFT: 'Left',		# Cursor left
		curses.KEY_RIGHT: 'Right',		# Cursor right
		curses.KEY_DC: 'Delete',		# Delete right
		curses.KEY_BACKSPACE: 'Backspace',	# Delete left
	}
	# Reverse mapping of KEYS - used for cooking key codes
	ESCSEQ = {
	}
	# Terminal output escape sequences
	CODES = {
		'DEOL': '',	# Delete to end of line
		'DEL': '',	# Delete and close up
		'INS': '',	# Insert space
		'CSRLEFT': '',	# Move cursor left 1 space
		'CSRRIGHT': '', # Move cursor right 1 space
	}
	# What prompt to display
	PROMPT = "Telnet Server> "
	# The function to call to verify authentication data
	authCallback = None
	# Does authCallback want a username?
	authNeedUser = False
	# Does authCallback want a password?
	authNeedPass = False

# --------------------------- Environment Setup ----------------------------

	def __init__(self, request, client_address, server):
		"""Constructor.

		When called without arguments, create an unconnected instance.
		With a hostname argument, it connects the instance; a port
		number is optional.
		"""
		# Am I doing the echoing?
		self.DOECHO = True
		# What opts have I sent DO/DONT for and what did I send?
		self.DOOPTS = {}
		# What opts have I sent WILL/WONT for and what did I send?
		self.WILLOPTS = {}
		# What commands does this CLI support
		self.COMMANDS = {}
		self.sock = None	# TCP socket
		self.rawq = ''		# Raw input string
		self.cookedq = []	# This is the cooked input stream (list of charcodes)
		self.sbdataq = ''	# Sub-Neg string
		self.eof = 0		# Has EOF been reached?
		self.iacseq = ''	# Buffer for IAC sequence.
		self.sb = 0		# Flag for SB and SE sequence.
		self.history = []	# Command history
		self.IQUEUELOCK = threading.Lock()
		self.OQUEUELOCK = threading.Lock()
		self.RUNSHELL = True
		# A little magic - Everything called cmdXXX is a command
		for k in dir(self):
			if k[:3] == 'cmd':
				name = k[3:]
				method = getattr(self, k)
				self.COMMANDS[name] = method
				for alias in getattr(method, "aliases", []):
					self.COMMANDS[alias] = self.COMMANDS[name]
		SocketServer.BaseRequestHandler.__init__(self, request, client_address, server)

	def setterm(self, term):
		"Set the curses structures for this terminal"
		logging.debug("Setting termtype to %s" % (term, ))
		curses.setupterm(term) # This will raise if the termtype is not supported
		self.TERM = term
		self.ESCSEQ = {}
		for k in self.KEYS.keys():
			str = curses.tigetstr(curses.has_key._capability_names[k])
			if str:
				self.ESCSEQ[str] = k
		self.CODES['DEOL'] = curses.tigetstr('el')
		self.CODES['DEL'] = curses.tigetstr('dch1')
		self.CODES['INS'] = curses.tigetstr('ich1')
		self.CODES['CSRLEFT'] = curses.tigetstr('cub1')
		self.CODES['CSRRIGHT'] = curses.tigetstr('cuf1')

	def setup(self):
		"Connect incoming connection to a telnet session"
		self.setterm(self.TERM)
		self.sock = self.request._sock
		for k in self.DOACK.keys():
			self.sendcommand(self.DOACK[k], k)
		for k in self.WILLACK.keys():
			self.sendcommand(self.WILLACK[k], k)
		self.thread_ic = threading.Thread(target=self.inputcooker)
		self.thread_ic.setDaemon(True)
		self.thread_ic.start()
		# Sleep for 0.5 second to allow options negotiation
		time.sleep(0.5)

	def finish(self):
		"End this session"
		self.sock.shutdown(socket.SHUT_RDWR)

# ------------------------- Telnet Options Engine --------------------------

	def options_handler(self, sock, cmd, opt):
		"Negotiate options"
#		if CMDS.has_key(cmd):
#			cmdtxt = CMDS[cmd]
#		else:
#			cmdtxt = "cmd:%d" % ord(cmd)
#		if cmd in [WILL, WONT, DO, DONT]:
#			if CMDS.has_key(opt):
#				opttxt = CMDS[opt]
#			else:
#				opttxt = "opt:%d" % ord(opt)
#		else:
#			opttxt = ""
#		logging.debug("OPTION: %s %s" % (cmdtxt, opttxt, ))
		if cmd == NOP:
			self.sendcommand(NOP)
		elif cmd == WILL or cmd == WONT:
			if self.WILLACK.has_key(opt):
				self.sendcommand(self.WILLACK[opt], opt)
			else:
				self.sendcommand(DONT, opt)
			if cmd == WILL and opt == TTYPE:
				self.writecooked(IAC + SB + TTYPE + SEND + IAC + SE)
		elif cmd == DO or cmd == DONT:
			if self.DOACK.has_key(opt):
				self.sendcommand(self.DOACK[opt], opt)
			else:
				self.sendcommand(WONT, opt)
			if opt == ECHO:
				self.DOECHO = (cmd == DO)
		elif cmd == SE:
			subreq = self.read_sb_data()
			if subreq[0] == TTYPE and subreq[1] == IS:
				try:
					self.setterm(subreq[2:])
				except:
					logging.debug("Terminal type not known")
		elif cmd == SB:
			pass
		else:
			logging.debug("Unhandled option: %s %s" % (cmdtxt, opttxt, ))

	def sendcommand(self, cmd, opt=None):
		"Send a telnet command (IAC)"
#		if CMDS.has_key(cmd):
#			cmdtxt = CMDS[cmd]
#		else:
#			cmdtxt = "cmd:%d" % ord(cmd)
#		if opt == None:
#			opttxt = ''
#		else:
#			if CMDS.has_key(opt):
#				opttxt = CMDS[opt]
#			else:
#				opttxt = "opt:%d" % ord(opt)
		if cmd in [DO, DONT]:
			if not self.DOOPTS.has_key(opt):
				self.DOOPTS[opt] = None
			if (((cmd == DO) and (self.DOOPTS[opt] != True))
			or ((cmd == DONT) and (self.DOOPTS[opt] != False))):
#				logging.debug("Sending %s %s" % (cmdtxt, opttxt, ))
				self.DOOPTS[opt] = (cmd == DO)
				self.writecooked(IAC + cmd + opt)
#			else:
#				logging.debug("Not resending %s %s" % (cmdtxt, opttxt, ))
		elif cmd in [WILL, WONT]:
			if not self.WILLOPTS.has_key(opt):
				self.WILLOPTS[opt] = ''
			if (((cmd == WILL) and (self.WILLOPTS[opt] != True))
			or ((cmd == WONT) and (self.WILLOPTS[opt] != False))):
#				logging.debug("Sending %s %s" % (cmdtxt, opttxt, ))
				self.WILLOPTS[opt] = (cmd == WILL)
				self.writecooked(IAC + cmd + opt)
#			else:
#				logging.debug("Not resending %s %s" % (cmdtxt, opttxt, ))
		else:
			self.writecooked(IAC + cmd)

	def read_sb_data(self):
		"""Return any data available in the SB ... SE queue.

		Return '' if no SB ... SE available. Should only be called
		after seeing a SB or SE command. When a new SB command is
		found, old unread SB data will be discarded. Don't block.

		"""
		buf = self.sbdataq
		self.sbdataq = ''
		return buf

# ---------------------------- Input Functions -----------------------------

	def _readline_echo(self, char, echo):
		"""Echo a recieved character, move cursor etc..."""
		if echo == True or (echo == None and self.DOECHO == True):
			self.write(char)

	def readline(self, echo=None):
		"""Return a line of text, including the terminating LF
		   If echo is true always echo, if echo is false never echo
		   If echo is None follow the negotiated setting.
		"""
		line = []
		insptr = 0
		histptr = len(self.history)
		while True:
			c = self.getc(block=True)
			if c == theNULL:
				continue
			elif c == curses.KEY_LEFT:
				if insptr > 0:
					insptr = insptr - 1
					self._readline_echo(self.CODES['CSRLEFT'], echo)
				else:
					self._readline_echo(chr(7), echo)
				continue
			elif c == curses.KEY_RIGHT:
				if insptr < len(line):
					insptr = insptr + 1
					self._readline_echo(self.CODES['CSRRIGHT'], echo)
				else:
					self._readline_echo(chr(7), echo)
				continue
			elif c == curses.KEY_UP or c == curses.KEY_DOWN:
				if c == curses.KEY_UP:
					if histptr > 0:
						histptr = histptr - 1
					else:
						self._readline_echo(chr(7), echo)
						continue
				elif c == curses.KEY_DOWN:
					if histptr < len(self.history):
						histptr = histptr + 1
					else:
						self._readline_echo(chr(7), echo)
						continue
				line = []
				if histptr < len(self.history):
					line.extend(self.history[histptr])
				for char in range(insptr):
					self._readline_echo(self.CODES['CSRLEFT'], echo)
				self._readline_echo(self.CODES['DEOL'], echo)
				self._readline_echo(''.join(line), echo)
				insptr = len(line)
				continue
			elif c == chr(3):
				self._readline_echo('\n' + curses.ascii.unctrl(c) + ' ABORT\n', echo)
				return ''
			elif c == chr(4):
				if len(line) > 0:
					self._readline_echo('\n' + curses.ascii.unctrl(c) + ' ABORT (QUIT)\n', echo)
					return ''
				self._readline_echo('\n' + curses.ascii.unctrl(c) + ' QUIT\n', echo)
				return 'QUIT'
			elif c == chr(10):
				self._readline_echo(c, echo)
				if echo == True or (echo == None and self.DOECHO == True):
					self.history.append(line)
				return ''.join(line)
			elif c == curses.KEY_BACKSPACE or c == chr(127) or c == chr(8):
				if insptr > 0:
					self._readline_echo(self.CODES['CSRLEFT'] + self.CODES['DEL'], echo)
					insptr = insptr - 1
					del line[insptr]
				else:
					self._readline_echo(chr(7), echo)
				continue
			elif c == curses.KEY_DC:
				if insptr < len(line):
					self._readline_echo(self.CODES['DEL'], echo)
					del line[insptr]
				else:
					self._readline_echo(chr(7), echo)
				continue
			else:
				if ord(c) < 32:
					c = curses.ascii.unctrl(c)
				self._readline_echo(c, echo)
			line[insptr:insptr] = c
			insptr = insptr + len(c)

	def getc(self, block=True):
		"""Return one character from the input queue"""
		if not block:
			if not len(self.cookedq):
				return ''
		while not len(self.cookedq):
			time.sleep(0.05)
		self.IQUEUELOCK.acquire()
		ret = self.cookedq[0]
		self.cookedq = self.cookedq[1:]
		self.IQUEUELOCK.release()
		return ret

# --------------------------- Output Functions -----------------------------

	def writeline(self, text):
		"""Send a packet with line ending."""
		self.write(text+chr(10))

	def write(self, text):
		"""Send a packet to the socket. This function cooks output."""
		text = text.replace(IAC, IAC+IAC)
		text = text.replace(chr(10), chr(13)+chr(10))
		self.writecooked(text)

	def writecooked(self, text):
		"""Put data directly into the output queue (bypass output cooker)"""
		self.OQUEUELOCK.acquire()
		self.sock.sendall(text)
		self.OQUEUELOCK.release()

# ------------------------------- Input Cooker -----------------------------

	def _inputcooker_getc(self, block=True):
		"""Get one character from the raw queue. Optionally blocking.
		Raise EOFError on end of stream. SHOULD ONLY BE CALLED FROM THE
		INPUT COOKER."""
		if self.rawq:
			ret = self.rawq[0]
			self.rawq = self.rawq[1:]
			return ret
		if not block:
			if select.select([self.sock.fileno()], [], [], 0) == ([], [], []):
				return ''
		ret = self.sock.recv(20)
		self.eof = not(ret)
		self.rawq = self.rawq + ret
		if self.eof:
			raise EOFError
		return self._inputcooker_getc(block)

	def _inputcooker_ungetc(self, char):
		"""Put characters back onto the head of the rawq. SHOULD ONLY
		BE CALLED FROM THE INPUT COOKER."""
		self.rawq = char + self.rawq

	def _inputcooker_store(self, char):
		"""Put the cooked data in the correct queue (with locking)"""
		if self.sb:
			self.sbdataq = self.sbdataq + char
		else:
			self.IQUEUELOCK.acquire()
			if type(char) in [type(()), type([]), type("")]:
				for v in char:
					self.cookedq.append(v)
			else:
				self.cookedq.append(char)
			self.IQUEUELOCK.release()

	def inputcooker(self):
		"""Input Cooker - Transfer from raw queue to cooked queue.

		Set self.eof when connection is closed.  Don't block unless in
		the midst of an IAC sequence.
		"""
		try:
			while True:
				c = self._inputcooker_getc()
				if not self.iacseq:
					if c == IAC:
						self.iacseq += c
						continue
					elif c == chr(13) and not(self.sb):
						c2 = self._inputcooker_getc(block=False)
						if c2 == theNULL or c2 == '':
							c = chr(10)
						elif c2 == chr(10):
							c = c2
						else:
							self._inputcooker_ungetc(c2)
							c = chr(10)
					elif c in [x[0] for x in self.ESCSEQ.keys()]:
						'Looks like the begining of a key sequence'
						codes = c
						for keyseq in self.ESCSEQ.keys():
							if len(keyseq) == 0:
								continue
							while codes == keyseq[:len(codes)] and len(codes) <= keyseq:
								if codes == keyseq:
									c = self.ESCSEQ[keyseq]
									break
								codes = codes + self._inputcooker_getc()
							if codes == keyseq:
								break
							self._inputcooker_ungetc(codes[1:])
							codes = codes[0]
					self._inputcooker_store(c)
				elif len(self.iacseq) == 1:
					'IAC: IAC CMD [OPTION only for WILL/WONT/DO/DONT]'
					if c in (DO, DONT, WILL, WONT):
						self.iacseq += c
						continue
					self.iacseq = ''
					if c == IAC:
						self._inputcooker_store(c)
					else:
						if c == SB: # SB ... SE start.
							self.sb = 1
							self.sbdataq = ''
	#							continue
						elif c == SE: # SB ... SE end.
							self.sb = 0
						# Callback is supposed to look into
						# the sbdataq
						self.options_handler(self.sock, c, NOOPT)
				elif len(self.iacseq) == 2:
					cmd = self.iacseq[1]
					self.iacseq = ''
					if cmd in (DO, DONT, WILL, WONT):
						self.options_handler(self.sock, cmd, c)
		except EOFError:
			pass

# ------------------------------- Basic Commands ---------------------------

# Format of docstrings for command methods:
# Line 0:  Command paramater(s) if any. (Can be blank line)
# Line 1:  Short descriptive text. (Mandatory)
# Line 2+: Long descriptive text. (Can be blank line)

	def cmdHELP(self, params):
		"""[<command>]
		Display help
		Display either brief help on all commands, or detailed
		help on a single command passed as a parameter.
		"""
		if params:
			cmd = params[0].upper()
			if self.COMMANDS.has_key(cmd):
				method = self.COMMANDS[cmd]
				doc = method.__doc__.split("\n")
				docp = doc[0].strip()
				docl = '\n'.join(doc[2:]).replace("\n\t\t", " ").replace("\t", "").strip()
				if len(docl) < 4:
					docl = doc[1].strip()
				self.writeline(
					"%s %s\n\n%s" % (
						cmd,
						docp,
						docl,
					)
				)
				return
			else:
				self.writeline("Command '%s' not known" % cmd)
		else:
			self.writeline("Help on built in commands\n")
		keys = self.COMMANDS.keys()
		keys.sort()
		for cmd in keys:
			method = self.COMMANDS[cmd]
			doc = method.__doc__.split("\n")
			docp = doc[0].strip()
			docs = doc[1].strip()
			if len(docp) > 0:
				docps = "%s - %s" % (docp, docs, )
			else:
				docps = "- %s" % (docs, )
			self.writeline(
				"%s %s" % (
					cmd,
					docps,
				)
			)
	cmdHELP.aliases = ['?']

	def cmdEXIT(self, params):
		"""
		Exit the command shell
		"""
		self.RUNSHELL = False
		self.writeline("Goodbye")
	cmdEXIT.aliases = ['QUIT', 'BYE', 'LOGOUT']

	def cmdDEBUG(self, params):
		"""
		Display some debugging data
		"""
		for (v,k) in self.ESCSEQ.items():
			line = '%-10s : ' % (self.KEYS[k], )
			for c in v:
				if ord(c)<32 or ord(c)>126:
					line = line + curses.ascii.unctrl(c)
				else:
					line = line + c
			self.writeline(line)

	def cmdHISTORY(self, params):
		"""
		Display the command history
		"""
		cnt = 0
		self.writeline('Command history\n')
		for line in self.history:
			cnt = cnt + 1
			self.writeline("%-5d : %s" % (cnt, ''.join(line)))

# ----------------------- Command Line Processor Engine --------------------

	def handleException(self, exc_type, exc_param, exc_tb):
		"Exception handler (False to abort)"
		self.writeline(traceback.format_exception_only(exc_type, exc_param)[-1])
		return True

	def handle(self):
		"The actual service to which the user has connected."
		username = None
		password = None
		if self.authCallback:
			if self.authNeedUser:
				if self.DOECHO:
					self.write("Username: ")
				username = self.readline()
			if self.authNeedPass:
				if self.DOECHO:
					self.write("Password: ")
				password = self.readline(echo=False)
				if self.DOECHO:
					self.write("\n")
			try:
				self.authCallback(username, password)
			except:
				return
		while self.RUNSHELL:
			if self.DOECHO:
				self.write(self.PROMPT)
			cmdlist = [item.strip() for item in self.readline().split()]
			idx = 0
			while idx < (len(cmdlist) - 1):
				if cmdlist[idx][0] in ["'", '"']:
					cmdlist[idx] = cmdlist[idx] + " " + cmdlist.pop(idx+1)
					if cmdlist[idx][0] != cmdlist[idx][-1]:
						continue
					cmdlist[idx] = cmdlist[idx][1:-1]
				idx = idx + 1
			if cmdlist:
				cmd = cmdlist[0].upper()
				params = cmdlist[1:]
				if self.COMMANDS.has_key(cmd):
					try:
						self.COMMANDS[cmd](params)
					except:
						(t, p, tb) = sys.exc_info()
						if self.handleException(t, p, tb):
							break
				else:
					self.write("Unknown command '%s'\n" % cmd)
		logging.debug("Exiting handler")

if __name__ == '__main__':
	"Testing - Accept a single connection"
	class TNS(SocketServer.TCPServer):
		allow_reuse_address = True

	class TNH(TelnetHandler):
		def cmdECHO(self, params):
			""" [<arg> ...]
			Echo parameters
			Echo command line parameters back to user, one per line.
			"""
			self.writeline("Parameters:")
			for item in params:
				self.writeline("\t%s" % item)

	logging.getLogger('').setLevel(logging.DEBUG)

	tns = TNS(("0.0.0.0", 23), TNH)
	tns.serve_forever()

# vim: set syntax=python ai showmatch: