예제 #1
0
def test_reject_negative_negotiation():
    opt = StreamParser.OptionNegotiation(
        OPTIONS.TM, OPTIONS.TM.value, StreamParser.Host.LOCAL, False
    )
    assert opt.refuse() == StreamParser.OptionNegotiation(
        OPTIONS.TM, OPTIONS.TM.value, StreamParser.Host.LOCAL, True
    )
예제 #2
0
def test_accept_negative_negotiation():
    opt = StreamParser.OptionNegotiation(
        OPTIONS.TM, OPTIONS.TM.value, StreamParser.Host.LOCAL, False
    )
    assert opt.accept() == StreamParser.OptionNegotiation(
        OPTIONS.TM, OPTIONS.TM.value, StreamParser.Host.LOCAL, False
    )
예제 #3
0
def test_stream_user_data_crlf(stream_parser):
    events = stream_parser.stream_updates([Tokenizer.StreamData(b"Hello,\r\nworld!")])
    assert events == [
        StreamParser.UserData("Hello,"),
        StreamParser.UserData("\n"),
        StreamParser.UserData("world!"),
    ]
예제 #4
0
    def __init__(self, reader, writer):
        self.reader = reader
        self.writer = writer

        self.encoder = StreamStuffer()
        self.tokenizer = Tokenizer()
        self.parser = StreamParser()
        self.line_buffer = LineBuffer()
        self.update_buffer = []
        self.prompt_mgr = None
        self.echo_state = EchoOptionState()
예제 #5
0
def test_stream_command_in_crlf(stream_parser):
    events = stream_parser.stream_updates(
        [
            Tokenizer.StreamData(b"abc\r"),
            Tokenizer.Command(B.NOP, B.NOP.value),
            Tokenizer.StreamData(b"\ndef"),
        ]
    )
    assert events == [
        StreamParser.UserData("abc"),
        StreamParser.Command(B.NOP, B.NOP.value),
        StreamParser.UserData("\n"),
        StreamParser.UserData("def"),
    ]
예제 #6
0
def test_stuffer_option_recognized(stuffer):
    stuffed = stuffer.stuff(
        StreamParser.OptionNegotiation(
            OPTIONS.TM, OPTIONS.TM.value, StreamParser.Host.PEER, False
        )
    )
    assert stuffed == (B.IAC.byte + B.DONT.byte + OPTIONS.TM.byte)
예제 #7
0
    def _write(self, text):
        if isinstance(text, str):
            text = StreamParser.UserData(text)

        out_data = self.encoder.stuff(text)
        logger.debug("writing", extra={"data": out_data})
        self.writer.write(out_data)
예제 #8
0
def test_stream_sb(stream_parser):
    events = stream_parser.stream_updates(
        [
            Tokenizer.Option(B.SB, None, 42),
            Tokenizer.StreamData(b"1234"),
            Tokenizer.Command(B.SE, B.SE.value),
        ]
    )
    assert events == [StreamParser.OptionSubnegotiation(None, 42)]
예제 #9
0
def test_stream_dont(stream_parser):
    events = stream_parser.stream_updates(
        [Tokenizer.Option(B.DONT, OPTIONS.TM, OPTIONS.TM.value)]
    )
    assert events == [
        StreamParser.OptionNegotiation(
            OPTIONS.TM, OPTIONS.TM.value, StreamParser.Host.LOCAL, False
        )
    ]
예제 #10
0
def test_stuffer_text(stuffer):
    stuffed = stuffer.stuff(StreamParser.UserData("abc"))
    assert stuffed == b"abc"
예제 #11
0
def test_stream_user_data(stream_parser):
    events = stream_parser.stream_updates([Tokenizer.StreamData(b"Hello, world!")])
    assert events == [StreamParser.UserData("Hello, world!")]
예제 #12
0
def stream_parser():
    return StreamParser()
예제 #13
0
def test_integration(tokenizer, stream_parser):
    data = (
        b"Hel"
        + B.IAC.byte
        + B.NOP.byte
        + b"lo,\r"
        +
        # start a subneg
        B.IAC.byte
        + B.SB.byte
        + bytes([42])
        + b"abc"
        +
        # literal IAC SE as subneg data
        B.IAC.byte
        + B.IAC.byte
        + B.SE.byte
        + b"def"
        +
        # finish the subneg
        B.IAC.byte
        + B.SE.byte
        + b"\0wor"
        + B.IAC.byte
        + B.DO.byte
        + bytes([42])
        + b"ld!"
    )
    atomized = [bytes([b]) for b in data]  # process it one byte at a time

    toks = sum([tokenizer.tokens(b) for b in atomized], [])
    events = sum([stream_parser.stream_updates([tok]) for tok in toks], [])

    assert events == [
        StreamParser.UserData("H"),
        StreamParser.UserData("e"),
        StreamParser.UserData("l"),
        StreamParser.Command(B.NOP, B.NOP.value),
        StreamParser.UserData("l"),
        StreamParser.UserData("o"),
        StreamParser.UserData(","),
        StreamParser.OptionSubnegotiation(None, 42),
        StreamParser.UserData("\r"),
        StreamParser.UserData("w"),
        StreamParser.UserData("o"),
        StreamParser.UserData("r"),
        StreamParser.OptionNegotiation(None, 42, StreamParser.Host.LOCAL, True),
        StreamParser.UserData("l"),
        StreamParser.UserData("d"),
        StreamParser.UserData("!"),
    ]
예제 #14
0
def test_stuffer_option_unrecognized(stuffer):
    stuffed = stuffer.stuff(
        StreamParser.OptionNegotiation(None, 42, StreamParser.Host.PEER, False)
    )
    assert stuffed == (B.IAC.byte + B.DONT.byte + bytes([42]))
예제 #15
0
def test_stuffer_crlf(stuffer):
    stuffed = stuffer.stuff(StreamParser.UserData("abc\ndef\rghi"))
    assert stuffed == b"abc\r\ndef\r\0ghi"
예제 #16
0
def test_stuffer_nonascii(stuffer):
    try:
        stuffed = stuffer.stuff(StreamParser.UserData("abcdéf"))
        assert False  # should have thrown an exception
    except UnicodeEncodeError:
        pass  # expected behavior under test
예제 #17
0
 def make_negotiation(self, state):
     return StreamParser.OptionNegotiation(OPTIONS.ECHO, OPTIONS.ECHO.value,
                                           StreamParser.Host.LOCAL, state)
예제 #18
0
def test_stream_user_data_nonascii(stream_parser):
    events = stream_parser.stream_updates([Tokenizer.StreamData(b"abc\xabdef")])
    assert events == [StreamParser.UserData("abcdef")]
예제 #19
0
def test_stream_wont(stream_parser):
    events = stream_parser.stream_updates([Tokenizer.Option(B.WONT, None, 42)])
    assert events == [
        StreamParser.OptionNegotiation(None, 42, StreamParser.Host.PEER, False)
    ]
예제 #20
0
def test_stream_will(stream_parser):
    events = stream_parser.stream_updates([Tokenizer.Option(B.WILL, None, 42)])
    assert events == [
        StreamParser.OptionNegotiation(None, 42, StreamParser.Host.PEER, True)
    ]
예제 #21
0
def test_stream_command(stream_parser):
    events = stream_parser.stream_updates([Tokenizer.Command(B.NOP, B.NOP.value)])
    assert events == [StreamParser.Command(B.NOP, B.NOP.value)]
예제 #22
0
class Terminal:
    READ_SIZE = 2**12  # arbitrary pleasant number?

    def __init__(self, reader, writer):
        self.reader = reader
        self.writer = writer

        self.encoder = StreamStuffer()
        self.tokenizer = Tokenizer()
        self.parser = StreamParser()
        self.line_buffer = LineBuffer()
        self.update_buffer = []
        self.prompt_mgr = None
        self.echo_state = EchoOptionState()

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_value, traceback):
        await self.close()

    async def close(self):
        await self.writer.drain()
        self.writer.close()
        await self.writer.wait_closed()

    async def sleep(self, secs):
        await self.writer.drain()
        await asyncio.sleep(secs)

    async def write(self, texts, drain=False):
        if texts is None:
            texts = []
        if not isinstance(texts, list):
            texts = [texts]

        for text in texts:
            self._write(text)
        if drain:
            await self.writer.drain()

    def _write(self, text):
        if isinstance(text, str):
            text = StreamParser.UserData(text)

        out_data = self.encoder.stuff(text)
        logger.debug("writing", extra={"data": out_data})
        self.writer.write(out_data)

    @contextlib.contextmanager
    def prompt(self, prompt):
        self.prompt_mgr = Prompt(prompt)
        try:
            yield
        finally:
            self.prompt_mgr = None

    async def input(self, prompt):
        with self.prompt(prompt):
            return await self._input_line()

    async def input_secret(self, prompt):
        with self.prompt(prompt):
            async with self.echo_off():
                result = await self._input_line()
                # echo peer's LF
                await self.write("\n")
                return result

    async def _input_line(self):
        while True:
            line = await self._handle_input_once()
            if line:
                return line

    async def _handle_input_once(self):
        await self.require_line_buffer()

        annotations, line = self.line_buffer.pop()
        for annotation in annotations:
            handler_name = "annotation_" + annotation.__class__.__name__
            handler = getattr(self, handler_name)
            await handler(annotation)

        return line

    async def require_line_buffer(self):
        while not self.line_buffer.has_line():
            await self.require_update_buffer()
            update = self.update_buffer.pop(0)
            await self.write(self.handle_update(update), drain=True)

    async def require_update_buffer(self):
        while not self.update_buffer:
            await self.write(self.prompt_mgr.require_has_prompt(), drain=True)
            self.update_buffer = await self.fetch_updates()

    async def fetch_updates(self):
        data = await self.reader.read(self.READ_SIZE)
        logger.debug("read", extra={"data": data})
        if not data:
            raise EOFError()

        toks = self.tokenizer.tokens(data)
        return self.parser.stream_updates(toks)

    async def annotation_TimingMark(self, annotation):
        await self.write(annotation.option.accept(), drain=True)

    #
    # locally requested state changes
    #

    @contextlib.asynccontextmanager
    async def echo_off(self):
        # The way we turn echo off is by turning on the server echo option
        # and then just not echoing anythiing.
        await self.write(self.echo_state.local_request(True), drain=True)
        try:
            yield
        finally:
            await self.write(self.echo_state.local_request(False), drain=True)

    #
    # input stream updates
    #

    def handle_update(self, update):
        handle = getattr(self, "update_" + update.__class__.__name__, None)
        if not handle:
            logger.info("unhandled update", extra={"update": update})
            return

        return handle(update)

    def update_UserData(self, update):
        self.line_buffer.append(update.data)
        self.prompt_mgr.mark_user_data()

    #
    # telnet options
    #

    def update_OptionNegotiation(self, request):
        handler = self.get_option_handler(request)
        return handler(request)

    def get_option_handler(self, request):
        if request.option is None:
            return self.option_unhandled

        handler_name = "option_" + request.option.name
        handler = getattr(self, "option_" + request.option.name, None)
        return handler or self.option_unhandled

    def option_TM(self, request):
        if not request.state:
            # client is rejecting this option. we only send it when it's
            # requested, so it's safe to ignore this.
            logger.debug("ignoring unmatched request",
                         extra={"request": request})
            return

        if request.host == StreamParser.Host.LOCAL:
            # client is requesting a TM.
            logger.info(f"ACCEPTING", extra={"request": request})
            self.line_buffer.annotate(self.TimingMark(request))
            return

        # otherwise client is sending us a TM, which we didn't request. ignore it.
        logger.debug("ignoring unrequested request",
                     extra={"request": request})

    def option_ECHO(self, request):
        return self.echo_state.handle_negotiation(request)

    def option_unhandled(self, request):
        if request.state:
            # Peer is requesting an unsupported option. Refuse.
            logger.info("rejecting option", extra={"request": request})
            return request.refuse()

        # else client is requesting to disable an option. we support no
        # options, so all are off. ignore it.
        else:
            logger.debug("ignoring option", extra={"request": request})

    # telnet commands

    def update_Command(self, command):
        handler = self.get_command_handler(command)
        return handler(command)

    def get_command_handler(self, command):
        if command.command is None:
            return self.command_unhandled

        handler_name = "command_" + command.command.name
        handler = getattr(self, handler_name, None)
        return handler or self.command_unhandled

    def command_unhandled(self, command):
        logger.info("unhandled command", extra={"command": command})

    def command_IP(self, command):
        logger.debug("interrupt process")
        # Currently we only ever read peer input during prompting, so
        # there's no process to interrupt. For bonus complexity there's a
        # fair chance the peer will send a timing mark request and discard
        # everything it gets before we acknowledge, so there's no point
        # sending a prompt unless we're sure they're ready. For no just
        # clear the input.
        self.line_buffer.clear()
        self.prompt_mgr.mark_interrupt()

    TimingMark = collections.namedtuple("TimingMark", ["option"])