""" Parser for the Telnet protocol. (Not a complete implementation of the telnet specification, but sufficient for a command line interface.) Inspired by `Twisted.conch.telnet`. """ from __future__ import annotations import struct from typing import Callable, Generator from .log import logger __all__ = [ "TelnetProtocolParser", ] def int2byte(number: int) -> bytes: return bytes((number,)) # Telnet constants. NOP = int2byte(0) SGA = int2byte(3) IAC = int2byte(255) DO = int2byte(253) DONT = int2byte(254) LINEMODE = int2byte(34) SB = int2byte(250) WILL = int2byte(251) WONT = int2byte(252) MODE = int2byte(1) SE = int2byte(240) ECHO = int2byte(1) NAWS = int2byte(31) LINEMODE = int2byte(34) SUPPRESS_GO_AHEAD = int2byte(3) TTYPE = int2byte(24) SEND = int2byte(1) IS = int2byte(0) DM = int2byte(242) BRK = int2byte(243) IP = int2byte(244) AO = int2byte(245) AYT = int2byte(246) EC = int2byte(247) EL = int2byte(248) GA = int2byte(249) class TelnetProtocolParser: """ Parser for the Telnet protocol. Usage:: def data_received(data): print(data) def size_received(rows, columns): print(rows, columns) p = TelnetProtocolParser(data_received, size_received) p.feed(binary_data) """ def __init__( self, data_received_callback: Callable[[bytes], None], size_received_callback: Callable[[int, int], None], ttype_received_callback: Callable[[str], None], ) -> None: self.data_received_callback = data_received_callback self.size_received_callback = size_received_callback self.ttype_received_callback = ttype_received_callback self._parser = self._parse_coroutine() self._parser.send(None) # type: ignore def received_data(self, data: bytes) -> None: self.data_received_callback(data) def do_received(self, data: bytes) -> None: """Received telnet DO command.""" logger.info("DO %r", data) def dont_received(self, data: bytes) -> None: """Received telnet DONT command.""" logger.info("DONT %r", data) def will_received(self, data: bytes) -> None: """Received telnet WILL command.""" logger.info("WILL %r", data) def wont_received(self, data: bytes) -> None: """Received telnet WONT command.""" logger.info("WONT %r", data) def command_received(self, command: bytes, data: bytes) -> None: if command == DO: self.do_received(data) elif command == DONT: self.dont_received(data) elif command == WILL: self.will_received(data) elif command == WONT: self.wont_received(data) else: logger.info("command received %r %r", command, data) def naws(self, data: bytes) -> None: """ Received NAWS. (Window dimensions.) """ if len(data) == 4: # NOTE: the first parameter of struct.unpack should be # a 'str' object. Both on Py2/py3. This crashes on OSX # otherwise. columns, rows = struct.unpack("!HH", data) self.size_received_callback(rows, columns) else: logger.warning("Wrong number of NAWS bytes") def ttype(self, data: bytes) -> None: """ Received terminal type. """ subcmd, data = data[0:1], data[1:] if subcmd == IS: ttype = data.decode("ascii") self.ttype_received_callback(ttype) else: logger.warning("Received a non-IS terminal type Subnegotiation") def negotiate(self, data: bytes) -> None: """ Got negotiate data. """ command, payload = data[0:1], data[1:] if command == NAWS: self.naws(payload) elif command == TTYPE: self.ttype(payload) else: logger.info("Negotiate (%r got bytes)", len(data)) def _parse_coroutine(self) -> Generator[None, bytes, None]: """ Parser state machine. Every 'yield' expression returns the next byte. """ while True: d = yield if d == int2byte(0): pass # NOP # Go to state escaped. elif d == IAC: d2 = yield if d2 == IAC: self.received_data(d2) # Handle simple commands. elif d2 in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA): self.command_received(d2, b"") # Handle IAC-[DO/DONT/WILL/WONT] commands. elif d2 in (DO, DONT, WILL, WONT): d3 = yield self.command_received(d2, d3) # Subnegotiation elif d2 == SB: # Consume everything until next IAC-SE data = [] while True: d3 = yield if d3 == IAC: d4 = yield if d4 == SE: break else: data.append(d4) else: data.append(d3) self.negotiate(b"".join(data)) else: self.received_data(d) def feed(self, data: bytes) -> None: """ Feed data to the parser. """ for b in data: self._parser.send(int2byte(b))