You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			210 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			210 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
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))
 |