From 4cd80284c91196b8ff9b780645fae275b3ed530b Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 20 Nov 2011 14:56:07 +0100 Subject: [PATCH] Implemented xterm256 colours. If not supported by client, converts to normal ANSI, as per kaViir's snippet. Integrates into Evennia's normal ansi parser and collaborates with TTYPE protocol to determine if the client supports it. --- src/comms/imc2lib/imc2_ansi.py | 74 ++++------ src/server/mssp.py | 4 - src/server/session.py | 2 +- src/server/telnet.py | 18 ++- src/server/ttype.py | 4 +- src/utils/ansi.py | 238 +++++++++++++++++++++------------ 6 files changed, 194 insertions(+), 146 deletions(-) diff --git a/src/comms/imc2lib/imc2_ansi.py b/src/comms/imc2lib/imc2_ansi.py index ee50ed112..d561e9329 100644 --- a/src/comms/imc2lib/imc2_ansi.py +++ b/src/comms/imc2lib/imc2_ansi.py @@ -6,58 +6,42 @@ This is a IMC2 complacent version. """ import re -from src.utils.ansi import ANSIParser, ANSITable, parse_ansi +from src.utils import ansi -class IMCANSIParser(ANSIParser): +class IMCANSIParser(ansi.ANSIParser): """ This parser is per the IMC2 specification. """ def __init__(self): + normal = ansi.ANSI_NORMAL + hilite = ansi.ANSI_HILITE self.ansi_map = [ - # Random - (r'~Z', ANSITable.ansi["normal"]), - # Dark Grey - (r'~D', ANSITable.ansi["hilite"] + ANSITable.ansi["black"]), - (r'~z', ANSITable.ansi["hilite"] + ANSITable.ansi["black"]), - # Grey/reset - (r'~w', ANSITable.ansi["normal"]), - (r'~d', ANSITable.ansi["normal"]), - (r'~!', ANSITable.ansi["normal"]), - # Bold/hilite - (r'~L', ANSITable.ansi["hilite"]), - # White - (r'~W', ANSITable.ansi["normal"] + ANSITable.ansi["hilite"]), - # Dark Green - (r'~g', ANSITable.ansi["normal"] + ANSITable.ansi["green"]), - # Green - (r'~G', ANSITable.ansi["hilite"] + ANSITable.ansi["green"]), - # Dark magenta - (r'~p', ANSITable.ansi["normal"] + ANSITable.ansi["magenta"]), - (r'~m', ANSITable.ansi["normal"] + ANSITable.ansi["magenta"]), - # Magenta - (r'~M', ANSITable.ansi["hilite"] + ANSITable.ansi["magenta"]), - (r'~P', ANSITable.ansi["hilite"] + ANSITable.ansi["magenta"]), - # Black - (r'~x', ANSITable.ansi["normal"] + ANSITable.ansi["black"]), - # Cyan - (r'~c', ANSITable.ansi["normal"] + ANSITable.ansi["cyan"]), - # Dark Yellow (brown) - (r'~Y', ANSITable.ansi["hilite"] + ANSITable.ansi["yellow"]), - # Yellow - (r'~y', ANSITable.ansi["normal"] + ANSITable.ansi["yellow"]), - # Dark Blue - (r'~B', ANSITable.ansi["normal"] + ANSITable.ansi["blue"]), - # Blue - (r'~C', ANSITable.ansi["hilite"] + ANSITable.ansi["blue"]), - # Dark Red - (r'~r', ANSITable.ansi["normal"] + ANSITable.ansi["red"]), - # Red - (r'~R', ANSITable.ansi["normal"] + ANSITable.ansi["red"]), - # Dark Blue - (r'~b', ANSITable.ansi["normal"] + ANSITable.ansi["blue"]), + (r'~Z', normal), # Random + (r'~x', normal + ansi.ANSI_BLACK), # Black + (r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey + (r'~z', hilite + ansi.ANSI_BLACK), + (r'~w', normal + ansi.ANSI_WHITE), # Grey + (r'~W', hilite + ansi.ANSI_WHITE), # White + (r'~g', normal + ansi.ANSI_GREEN), # Dark Green + (r'~G', hilite + ansi.ANSI_GREEN), # Green + (r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta + (r'~m', normal + ansi.ANSI_MAGENTA), + (r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta + (r'~P', hilite + ansi.ANSI_MAGENTA), + (r'~c', normal + ansi.ANSI_CYAN), # Cyan + (r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown) + (r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow + (r'~b', normal + ansi.ANSI_BLUE), # Dark Blue + (r'~B', hilite + ansi.ANSI_BLUE), # Blue + (r'~C', hilite + ansi.ANSI_BLUE), + (r'~r', normal + ansi.ANSI_RED), # Dark Red + (r'~R', hilite + ansi.ANSI_RED), # Red + ## Formatting - (r'\\r', ANSITable.ansi["normal"]), - (r'\\n', ANSITable.ansi["return"]), + (r'~L', hilite), # Bold/hilite + (r'~!', normal), # reset + (r'\\r', normal), + (r'\\n', ansi.ANSI_RETURN), ] # prepare regex matching self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1]) diff --git a/src/server/mssp.py b/src/server/mssp.py index 82d3b766a..65f991948 100644 --- a/src/server/mssp.py +++ b/src/server/mssp.py @@ -48,7 +48,6 @@ class Mssp(object): """ This is the normal operation. """ - print "no mssp" pass def do_mssp(self, option): @@ -182,6 +181,3 @@ class Mssp(object): # send to crawler by subnegotiation self.protocol.requestNegotiation(MSSP, varlist) - - - diff --git a/src/server/session.py b/src/server/session.py index 77dd69c3e..a4b71ebdb 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -36,7 +36,7 @@ class Session(object): _attrs_to_sync = ['protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname', 'logged_in', 'cid', 'encoding', 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', - 'protocol_flags', 'server_data'] + 'server_data'] def init_session(self, protocol_key, address, sessionhandler): """ diff --git a/src/server/telnet.py b/src/server/telnet.py index c83953e3a..b0e05c3af 100644 --- a/src/server/telnet.py +++ b/src/server/telnet.py @@ -36,9 +36,11 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # negotiate mssp (crawler communication) self.mssp = mssp.Mssp(self) - - # add us to sessionhandler - self.sessionhandler.connect(self) + + # add this new connection to sessionhandler so + # the Server becomes aware of it. + self.sessionhandler.connect(self) + def enableRemote(self, option): """ @@ -137,8 +139,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): string = utils.to_str(string, encoding=self.encoding) except Exception, e: self.sendLine(str(e)) - return - nomarkup = False + return + xterm256 = self.protocol_flags.get('TTYPE', {}).get('256 COLORS') + nomarkup = not (xterm256 or not self.protocol_flags.get('TTYPE') + or self.protocol_flags.get('TTYPE', {}).get('ANSI')) raw = False if type(data) == dict: # check if we want escape codes to go through unparsed. @@ -147,5 +151,5 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): nomarkup = data.get("nomarkup", False) if raw: self.sendLine(string) - else: - self.sendLine(ansi.parse_ansi(string, strip_ansi=nomarkup)) + else: + self.sendLine(ansi.parse_ansi(string, strip_ansi=nomarkup, xterm256=xterm256)) diff --git a/src/server/ttype.py b/src/server/ttype.py index 701b06c29..14c727a38 100644 --- a/src/server/ttype.py +++ b/src/server/ttype.py @@ -47,7 +47,7 @@ class Ttype(object): self.protocol.negotiationMap[TTYPE] = self.do_ttype # ask if client will ttype, connect callback if it does. self.protocol.will(TTYPE).addCallbacks(self.do_ttype, self.no_ttype) - + def no_ttype(self, option): """ Callback if ttype is not supported by client. @@ -79,7 +79,7 @@ class Ttype(object): elif self.ttype_step == 3: self.protocol.protocol_flags['TTYPE']['TERM'] = option self.protocol.requestNegotiation(TTYPE, SEND) - elif self.ttype_step == 4 and option.startswith('MTTS'): + elif self.ttype_step == 4: option = int(option.strip('MTTS ')) self.protocol.protocol_flags['TTYPE']['MTTS'] = option for codenum, standard in MTTS: diff --git a/src/utils/ansi.py b/src/utils/ansi.py index a9c02544f..b40f03780 100644 --- a/src/utils/ansi.py +++ b/src/utils/ansi.py @@ -16,51 +16,45 @@ user. import re from src.utils import utils -class ANSITable(object): - """ - A table defining the - standard ANSI command sequences. +# ANSI definitions - """ - ansi = {} - ansi["beep"] = "\07" - ansi["escape"] = "\033" - ansi["normal"] = "\033[0m" +ANSI_BEEP = "\07" +ANSI_ESCAPE = "\033" +ANSI_NORMAL = "\033[0m" - ansi["underline"] = "\033[4m" - ansi["hilite"] = "\033[1m" - ansi["blink"] = "\033[5m" - ansi["inverse"] = "\033[7m" - ansi["inv_hilite"] = "\033[1;7m" - ansi["inv_blink"] = "\033[7;5m" - ansi["blink_hilite"] = "\033[1;5m" - ansi["inv_blink_hilite"] = "\033[1;5;7m" +ANSI_UNDERLINE = "\033[4m" +ANSI_HILITE = "\033[1m" +ANSI_BLINK = "\033[5m" +ANSI_INVERSE = "\033[7m" +ANSI_INV_HILITE = "\033[1;7m" +ANSI_INV_BLINK = "\033[7;5m" +ANSI_BLINK_HILITE = "\033[1;5m" +ANSI_INV_BLINK_HILITE = "\033[1;5;7m" - # Foreground colors - ansi["black"] = "\033[30m" - ansi["red"] = "\033[31m" - ansi["green"] = "\033[32m" - ansi["yellow"] = "\033[33m" - ansi["blue"] = "\033[34m" - ansi["magenta"] = "\033[35m" - ansi["cyan"] = "\033[36m" - ansi["white"] = "\033[37m" +# Foreground colors +ANSI_BLACK = "\033[30m" +ANSI_RED = "\033[31m" +ANSI_GREEN = "\033[32m" +ANSI_YELLOW = "\033[33m" +ANSI_BLUE = "\033[34m" +ANSI_MAGENTA = "\033[35m" +ANSI_CYAN = "\033[36m" +ANSI_WHITE = "\033[37m" - # Background colors - ansi["back_black"] = "\033[40m" - ansi["back_red"] = "\033[41m" - ansi["back_green"] = "\033[42m" - ansi["back_yellow"] = "\033[43m" - ansi["back_blue"] = "\033[44m" - ansi["back_magenta"] = "\033[45m" - ansi["back_cyan"] = "\033[46m" - ansi["back_white"] = "\033[47m" - - # Formatting Characters - ansi["return"] = "\r\n" - ansi["tab"] = "\t" - ansi["space"] = " " +# Background colors +ANSI_BACK_BLACK = "\033[40m" +ANSI_BACK_RED = "\033[41m" +ANSI_BACK_GREEN = "\033[42m" +ANSI_BACK_YELLOW = "\033[43m" +ANSI_BACK_BLUE = "\033[44m" +ANSI_BACK_MAGENTA = "\033[45m" +ANSI_BACK_CYAN = "\033[46m" +ANSI_BACK_WHITE = "\033[47m" +# Formatting Characters +ANSI_RETURN = "\r\n" +ANSI_TAB = "\t" +ANSI_SPACE = " " class ANSIParser(object): """ @@ -74,64 +68,132 @@ class ANSIParser(object): # MUX-style mappings %cr %cn etc self.mux_ansi_map = [ - (r'%r', ANSITable.ansi["return"]), - (r'%t', ANSITable.ansi["tab"]), - (r'%b', ANSITable.ansi["space"]), - (r'%cf', ANSITable.ansi["blink"]), - (r'%ci', ANSITable.ansi["inverse"]), - (r'%ch', ANSITable.ansi["hilite"]), - (r'%cn', ANSITable.ansi["normal"]), - (r'%cx', ANSITable.ansi["black"]), - (r'%cX', ANSITable.ansi["back_black"]), - (r'%cr', ANSITable.ansi["red"]), - (r'%cR', ANSITable.ansi["back_red"]), - (r'%cg', ANSITable.ansi["green"]), - (r'%cG', ANSITable.ansi["back_green"]), - (r'%cy', ANSITable.ansi["yellow"]), - (r'%cY', ANSITable.ansi["back_yellow"]), - (r'%cb', ANSITable.ansi["blue"]), - (r'%cB', ANSITable.ansi["back_blue"]), - (r'%cm', ANSITable.ansi["magenta"]), - (r'%cM', ANSITable.ansi["back_magenta"]), - (r'%cc', ANSITable.ansi["cyan"]), - (r'%cC', ANSITable.ansi["back_cyan"]), - (r'%cw', ANSITable.ansi["white"]), - (r'%cW', ANSITable.ansi["back_white"]), + (r'%r', ANSI_RETURN), + (r'%t', ANSI_TAB), + (r'%b', ANSI_SPACE), + (r'%cf', ANSI_BLINK), + (r'%ci', ANSI_INVERSE), + (r'%ch', ANSI_HILITE), + (r'%cn', ANSI_NORMAL), + (r'%cx', ANSI_BLACK), + (r'%cX', ANSI_BACK_BLACK), + (r'%cr', ANSI_RED), + (r'%cR', ANSI_BACK_RED), + (r'%cg', ANSI_GREEN), + (r'%cG', ANSI_BACK_GREEN), + (r'%cy', ANSI_YELLOW), + (r'%cY', ANSI_BACK_YELLOW), + (r'%cb', ANSI_BLUE), + (r'%cB', ANSI_BACK_BLUE), + (r'%cm', ANSI_MAGENTA), + (r'%cM', ANSI_BACK_MAGENTA), + (r'%cc', ANSI_CYAN), + (r'%cC', ANSI_BACK_CYAN), + (r'%cw', ANSI_WHITE), + (r'%cW', ANSI_BACK_WHITE), ] # Expanded mapping {r {n etc - hilite = ANSITable.ansi['hilite'] - normal = ANSITable.ansi['normal'] + hilite = ANSI_HILITE + normal = ANSI_NORMAL self.ext_ansi_map = [ - (r'{r', hilite + ANSITable.ansi['red']), - (r'{R', normal + ANSITable.ansi['red']), - (r'{g', hilite + ANSITable.ansi['green']), - (r'{G', normal + ANSITable.ansi['green']), - (r'{y', hilite + ANSITable.ansi['yellow']), - (r'{Y', normal + ANSITable.ansi['yellow']), - (r'{b', hilite + ANSITable.ansi['blue']), - (r'{B', normal + ANSITable.ansi['blue']), - (r'{m', hilite + ANSITable.ansi['magenta']), - (r'{M', normal + ANSITable.ansi['magenta']), - (r'{c', hilite + ANSITable.ansi['cyan']), - (r'{C', normal + ANSITable.ansi['cyan']), - (r'{w', hilite + ANSITable.ansi['white']), #white - (r'{W', normal + ANSITable.ansi['white']), #light grey - (r'{x', hilite + ANSITable.ansi['black']), #dark grey - (r'{X', normal + ANSITable.ansi['black']), #pure black - (r'{n', normal) #reset + (r'{r', hilite + ANSI_RED), + (r'{R', normal + ANSI_RED), + (r'{g', hilite + ANSI_GREEN), + (r'{G', normal + ANSI_GREEN), + (r'{y', hilite + ANSI_YELLOW), + (r'{Y', normal + ANSI_YELLOW), + (r'{b', hilite + ANSI_BLUE), + (r'{B', normal + ANSI_BLUE), + (r'{m', hilite + ANSI_MAGENTA), + (r'{M', normal + ANSI_MAGENTA), + (r'{c', hilite + ANSI_CYAN), + (r'{C', normal + ANSI_CYAN), + (r'{w', hilite + ANSI_WHITE), # pure white + (r'{W', normal + ANSI_WHITE), #light grey + (r'{x', hilite + ANSI_BLACK), #dark grey + (r'{X', normal + ANSI_BLACK), #pure black + (r'{n', normal) #reset ] - self.ansi_map = self.mux_ansi_map + self.ext_ansi_map + # xterm256 + + self.xterm256_map = [ + (r'%c([1-5]{3})', self.parse_rgb), + (r'%c(b[1-5]{3})', self.parse_rgb), + (r'{([1-5]{3})', self.parse_rgb), + (r'{(b[1-5]{3})', self.parse_rgb) + ] + + # obs - order matters here, we want to do the xterms first since + # they collide with some of the other mappings otherwise. + self.ansi_map = self.xterm256_map + self.mux_ansi_map + self.ext_ansi_map # prepare regex matching self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1]) for sub in self.ansi_map] + # prepare matching ansi codes overall self.ansi_regex = re.compile("\033\[[0-9;]+m") - def parse_ansi(self, string, strip_ansi=False): + def parse_rgb(self, rgbmatch): + """ + This is a replacer method called by re.sub with the matched + tag. It must return the correct ansi sequence. + + It checks self.do_xterm256 to determine if conversion + to standard ansi should be done or not. + """ + if not rgbmatch: + return "" + rgbtag = rgbmatch.groups()[0] + + background = rgbtag[0] == 'b' + if background: + red, green, blue = int(rgbtag[1]), int(rgbtag[2]), int(rgbtag[3]) + else: + red, green, blue = int(rgbtag[0]), int(rgbtag[1]), int(rgbtag[2]) + + if self.do_xterm256: + colval = 16 + (red * 36) + (green * 6) + blue + return "\033[%s8;5;%s%s%sm" % (3 + int(background), colval/100, (colval%100)/10, colval%10) + else: + # xterm256 not supported, convert the rgb value to ansi instead + if red == green and red == blue and red < 2: + if background: return ANSI_BACK_BLACK + elif red >= 1: return ANSI_HILITE + ANSI_BLACK + else: return ANSO_NORMAL + ANSI_BLACK + elif red == green and red == blue: + if background: return ANSI_BACK_WHITE + elif red >= 4: return ANSI_HILITE + ANSI_WHITE + else: return ANSI_NORMAL + ANSI_WHITE + elif red > green and red > blue: + if background: return ANSI_BACK_RED + elif red >= 3: return ANSI_HILITE + ANSI_RED + else: return ANSI_NORMAL + ANSI_RED + elif red == green and red > blue: + if background: return ANSI_BACK_YELLOW + elif red >= 3: return ANSI_HILITE + ANSI_YELLOW + else: return ANSI_NORMAL + ANSI_YELLOW + elif red == blue and red > green: + if background: return ANSI_BACK_MAGENTA + elif red >= 3: return ANSI_HILITE + ANSI_MAGENTA + else: return ANSI_NORMAL + ANSI_MAGENTA + elif green > blue: + if background: return ANSI_BACK_GREEN + elif green >= 3: return ANSI_HILITE + ANSI_GREEN + else: return ANSI_NORMAL + ANSI_GREEN + elif green == blue: + if background: return ANSI_BACK_CYAN + elif green >= 3: return ANSI_HILITE + ANSI_CYAN + else: return ANSI_NORMAL + ANSI_CYAN + else: # mostly blue + if background: return ANSI_BACK_BLUE + elif blue >= 3: return ANSI_HILITE + ANSI_BLUE + else: return ANSI_NORMAL + ANSI_BLUE + + def parse_ansi(self, string, strip_ansi=False, xterm256=False): """ Parses a string, subbing color codes according to the stored mapping. @@ -142,6 +204,9 @@ class ANSIParser(object): if not string: return '' string = utils.to_str(string) + + self.do_xterm256 = xterm256 + # handle all subs for sub in self.ansi_sub: # go through all available mappings and translate them string = sub[0].sub(sub[1], string) @@ -150,7 +215,6 @@ class ANSIParser(object): string = self.ansi_regex.sub("", string) return string - ANSI_PARSER = ANSIParser() @@ -158,11 +222,11 @@ ANSI_PARSER = ANSIParser() # Access function # -def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER): +def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False): """ Parses a string, subbing color codes as needed. """ - return parser.parse_ansi(string, strip_ansi=strip_ansi) + return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256)