Ejemplo n.º 1
0
class TelnetConnection(object):
    """
    Class that represents one Telnet connection.
    """
    def __init__(self, conn, addr, application, server, encoding):
        assert isinstance(addr, tuple)  # (addr, port) tuple
        assert isinstance(application, TelnetApplication)
        assert isinstance(server, TelnetServer)
        assert isinstance(encoding, text_type)  # e.g. 'utf-8'

        self.conn = conn
        self.addr = addr
        self.application = application
        self.closed = False
        self.handling_command = True
        self.server = server
        self.encoding = encoding
        self.callback = None  # Function that handles the CLI result.

        # Create "Output" object.
        self.size = Size(rows=40, columns=79)

        # Initialize.
        _initialize_telnet(conn)

        # Create output.
        def get_size():
            return self.size

        self.stdout = _ConnectionStdout(conn, encoding=encoding)
        self.vt100_output = Vt100_Output(self.stdout, get_size)

        # Create an eventloop (adaptor) for the CommandLineInterface.
        self.eventloop = _TelnetEventLoopInterface(server)

        # Set default CommandLineInterface.
        self.set_application(create_prompt_application())

        # Call client_connected
        application.client_connected(self)

        # Draw for the first time.
        self.handling_command = False
        self.cli._redraw()

    def set_application(self, app, callback=None):
        """
        Set ``CommandLineInterface`` instance for this connection.
        (This can be replaced any time.)

        :param cli: CommandLineInterface instance.
        :param callback: Callable that takes the result of the CLI.
        """
        assert isinstance(app, Application)
        assert callback is None or callable(callback)

        self.cli = CommandLineInterface(application=app,
                                        eventloop=self.eventloop,
                                        output=self.vt100_output)
        self.callback = callback

        # Create a parser, and parser callbacks.
        cb = self.cli.create_eventloop_callbacks()
        inputstream = InputStream(cb.feed_key)

        # Input decoder for stdin. (Required when working with multibyte
        # characters, like chinese input.)
        stdin_decoder_cls = getincrementaldecoder(self.encoding)
        stdin_decoder = [stdin_decoder_cls()]  # nonlocal

        def data_received(data):
            """ TelnetProtocolParser 'data_received' callback """
            assert isinstance(data, binary_type)

            try:
                result = stdin_decoder[0].decode(data)
                inputstream.feed(result)
            except UnicodeDecodeError:
                stdin_decoder[0] = stdin_decoder_cls()
                return ''

        def size_received(rows, columns):
            """ TelnetProtocolParser 'size_received' callback """
            self.size = Size(rows=rows, columns=columns)
            cb.terminal_size_changed()

        self.parser = TelnetProtocolParser(data_received, size_received)

    def feed(self, data):
        """
        Handler for incoming data. (Called by TelnetServer.)
        """
        assert isinstance(data, binary_type)

        self.parser.feed(data)

        # Render again.
        self.cli._redraw()

        # When a return value has been set (enter was pressed), handle command.
        if self.cli.is_returning:
            try:
                return_value = self.cli.return_value()
            except (EOFError, KeyboardInterrupt) as e:
                # Control-D or Control-C was pressed.
                logger.info('%s, closing connection.', type(e).__name__)
                self.close()
                return

            # Handle CLI command
            self._handle_command(return_value)

    def _handle_command(self, command):
        """
        Handle command. This will run in a separate thread, in order not
        to block the event loop.
        """
        logger.info('Handle command %r', command)

        def in_executor():
            self.handling_command = True
            try:
                if self.callback is not None:
                    self.callback(self, command)
            finally:
                self.server.call_from_executor(done)

        def done():
            self.handling_command = False

            # Reset state and draw again. (If the connection is still open --
            # the application could have called TelnetConnection.close()
            if not self.closed:
                self.cli.reset()
                self.cli.buffers[DEFAULT_BUFFER].reset()
                self.cli.renderer.request_absolute_cursor_position()
                self.vt100_output.flush()
                self.cli._redraw()

        self.server.run_in_executor(in_executor)

    def erase_screen(self):
        """
        Erase output screen.
        """
        self.vt100_output.erase_screen()
        self.vt100_output.cursor_goto(0, 0)
        self.vt100_output.flush()

    def send(self, data):
        """
        Send text to the client.
        """
        assert isinstance(data, text_type)

        # When data is send back to the client, we should replace the line
        # endings. (We didn't allocate a real pseudo terminal, and the telnet
        # connection is raw, so we are responsible for inserting \r.)
        self.stdout.write(data.replace('\n', '\r\n'))
        self.stdout.flush()

    def close(self):
        """
        Close the connection.
        """
        self.application.client_leaving(self)

        self.conn.close()
        self.closed = True
Ejemplo n.º 2
0
class ShellConnection(TelnetConnection):
    def __init__(self, reader, writer, shell, loop):
        super(ShellConnection, self).__init__(reader, writer)
        self._shell = shell
        self._loop = loop
        self._cli = None
        self._cb = None
        self._size = Size(rows=40, columns=79)
        self.encoding = 'utf-8'

    @asyncio.coroutine
    def connected(self):
        # prompt_toolkit internally checks if it's on windows during output rendering but
        # we need to force that we use Vt100_Output not Win32_Output
        from prompt_toolkit import renderer
        renderer.is_windows = lambda: False

        def get_size():
            return self._size

        self._cli = CommandLineInterface(
            application=create_prompt_application(self._shell.prompt),
            eventloop=UnstoppableEventLoop(create_asyncio_eventloop(
                self._loop)),
            input=PatchedStdinInput(sys.stdin),
            output=Vt100_Output(self, get_size))

        self._cb = self._cli.create_eventloop_callbacks()
        self._inputstream = InputStream(self._cb.feed_key)
        # Taken from prompt_toolkit telnet server
        # https://github.com/jonathanslenders/python-prompt-toolkit/blob/99fa7fae61c9b4ed9767ead3b4f9b1318cfa875d/prompt_toolkit/contrib/telnet/server.py#L165
        self._cli._is_running = True

        if self._shell.welcome_message is not None:
            self.send(self._shell.welcome_message.encode())

        self._cli._redraw()

    @asyncio.coroutine
    def disconnected(self):
        pass

    def window_size_changed(self, columns, rows):
        self._size = Size(rows=rows, columns=columns)
        self._cb.terminal_size_changed()

    @asyncio.coroutine
    def feed(self, data):
        data = data.decode()
        self._inputstream.feed(data)
        self._cli._redraw()

        # Prompt toolkit has returned the command
        if self._cli.is_returning:
            try:
                returned_value = self._cli.return_value()
            except (EOFError, KeyboardInterrupt) as e:
                # don't close terminal, just keep it alive
                self.close()
                return

            command = returned_value.text

            res = yield from self._shell._parse_command(command)
            self.send(res.encode())
            self.reset()

    def reset(self):
        """ Resets terminal screen"""
        self._cli.reset()
        self._cli.buffers[DEFAULT_BUFFER].reset()
        self._cli.renderer.request_absolute_cursor_position()
        self._cli._redraw()

    def write(self, data):
        """ Compat with CLI"""
        self.send(data)

    def flush(self):
        """ Compat with CLI"""
        pass
Ejemplo n.º 3
0
class ReplSSHServerSession(asyncssh.SSHServerSession):

    """
    SSH server session that runs a Python REPL.

    :param get_globals: callable that returns the current globals.
    :param get_locals: (optional) callable that returns the current locals.
    """

    def __init__(self, get_globals, get_locals=None):
        assert callable(get_globals)
        assert get_locals is None or callable(get_locals)

        self._chan = None

        def _globals():
            data = get_globals()
            data.setdefault('print', self._print)
            return data

        repl = PythonRepl(get_globals=_globals,
                          get_locals=get_locals or _globals)

        # Disable open-in-editor and system prompt. Because it would run and
        # display these commands on the server side, rather than in the SSH
        # client.
        repl.enable_open_in_editor = False
        repl.enable_system_bindings = False

        # PipInput object, for sending input in the CLI.
        # (This is something that we can use in the prompt_toolkit event loop,
        # but still write date in manually.)
        self._input_pipe = PipeInput()

        # Output object. Don't render to the real stdout, but write everything
        # in the SSH channel.
        class Stdout(object):

            def write(s, data):
                if self._chan is not None:
                    self._chan.write(data.replace('\n', '\r\n'))

            def flush(s):
                pass

        # Create command line interface.
        self.cli = CommandLineInterface(
            application=repl.create_application(),
            eventloop=create_asyncio_eventloop(),
            input=self._input_pipe,
            output=Vt100_Output(Stdout(), self._get_size))

        self._callbacks = self.cli.create_eventloop_callbacks()

    def _get_size(self):
        """
        Callable that returns the current `Size`, required by Vt100_Output.
        """
        if self._chan is None:
            return Size(rows=20, columns=79)
        else:
            width, height, pixwidth, pixheight = self._chan.get_terminal_size()
            return Size(rows=height, columns=width)

    def connection_made(self, chan):
        """
        Client connected, run repl in coroutine.
        """
        self._chan = chan

        # Run REPL interface.
        f = asyncio.async(self.cli.run_async())

        # Close channel when done.
        def done(_):
            chan.close()
            self._chan = None
        f.add_done_callback(done)

    def shell_requested(self):
        return True

    def terminal_size_changed(self, width, height, pixwidth, pixheight):
        """
        When the terminal size changes, report back to CLI.
        """
        self._callbacks.terminal_size_changed()

    def data_received(self, data, datatype):
        """
        When data is received, send to inputstream of the CLI and repaint.
        """
        self._input_pipe.send(data)

    def _print(self, *data, **kw):
        """
        _print(self, *data, sep=' ', end='\n', file=None)

        Alternative 'print' function that prints back into the SSH channel.
        """
        # Pop keyword-only arguments. (We cannot use the syntax from the
        # signature. Otherwise, Python2 will give a syntax error message when
        # installing.)
        sep = kw.pop('sep', ' ')
        end = kw.pop('end', '\n')
        file = kw.pop('end', None)
        assert not kw, 'Too many keyword-only arguments'

        data = sep.join(map(str, data))
        self._chan.write(data + end)
Ejemplo n.º 4
0
class ShellConnection(TelnetConnection):
    def __init__(self, reader, writer, shell, window_size_changed_callback, loop):
        super(ShellConnection, self).__init__(reader, writer, window_size_changed_callback)
        self._shell = shell
        self._loop = loop
        self._cli = None
        self._cb = None
        self._size = Size(rows=40, columns=79)
        self.encoding = 'utf-8'


    async def connected(self):
        # prompt_toolkit internally checks if it's on windows during output rendering but
        # we need to force that we use Vt100_Output not Win32_Output
        from prompt_toolkit import renderer
        renderer.is_windows = lambda: False

        def get_size():
            return self._size

        self._cli = CommandLineInterface(
            application=create_prompt_application(self._shell.prompt),
            eventloop=UnstoppableEventLoop(create_asyncio_eventloop(self._loop)),
            input=PatchedStdinInput(sys.stdin),
            output=Vt100_Output(self, get_size))

        self._cb = self._cli.create_eventloop_callbacks()
        self._inputstream = InputStream(self._cb.feed_key)
        # Taken from prompt_toolkit telnet server
        # https://github.com/jonathanslenders/python-prompt-toolkit/blob/99fa7fae61c9b4ed9767ead3b4f9b1318cfa875d/prompt_toolkit/contrib/telnet/server.py#L165
        self._cli._is_running = True

        if self._shell.welcome_message is not None:
            self.send(self._shell.welcome_message.encode())

        self._cli._redraw()

    async def disconnected(self):
        pass

    @asyncio.coroutine
    def window_size_changed(self, columns, rows):
        self._size = Size(rows=rows, columns=columns)
        self._cb.terminal_size_changed()
        if self._window_size_changed_callback:
            yield from self._window_size_changed_callback(columns, rows)

    async def feed(self, data):
        data = data.decode()
        self._inputstream.feed(data)
        self._cli._redraw()

        # Prompt toolkit has returned the command
        if self._cli.is_returning:
            try:
                returned_value = self._cli.return_value()
            except (EOFError, KeyboardInterrupt) as e:
                # don't close terminal, just keep it alive
                self.close()
                return

            command = returned_value.text

            res = await self._shell._parse_command(command)
            self.send(res.encode())
            self.reset()

    def reset(self):
        """ Resets terminal screen"""
        self._cli.reset()
        self._cli.buffers[DEFAULT_BUFFER].reset()
        self._cli.renderer.request_absolute_cursor_position()
        self._cli._redraw()

    def write(self, data):
        """ Compat with CLI"""
        self.send(data)

    def flush(self):
        """ Compat with CLI"""
        pass
Ejemplo n.º 5
0
class TelnetConnection(object):
    """
    Class that represents one Telnet connection.
    """

    def __init__(self, conn, addr, application, server, encoding):
        assert isinstance(addr, tuple)  # (addr, port) tuple
        assert isinstance(application, TelnetApplication)
        assert isinstance(server, TelnetServer)
        assert isinstance(encoding, text_type)  # e.g. 'utf-8'

        self.conn = conn
        self.addr = addr
        self.application = application
        self.closed = False
        self.handling_command = True
        self.server = server
        self.encoding = encoding
        self.callback = None  # Function that handles the CLI result.

        # Create "Output" object.
        self.size = Size(rows=40, columns=79)

        # Initialize.
        _initialize_telnet(conn)

        # Create output.
        def get_size():
            return self.size

        self.stdout = _ConnectionStdout(conn, encoding=encoding)
        self.vt100_output = Vt100_Output(self.stdout, get_size)

        # Create an eventloop (adaptor) for the CommandLineInterface.
        self.eventloop = _TelnetEventLoopInterface(server)

        # Set default CommandLineInterface.
        self.set_application(create_prompt_application())

        # Call client_connected
        application.client_connected(self)

        # Draw for the first time.
        self.handling_command = False
        self.cli._redraw()

    def set_application(self, app, callback=None):
        """
        Set ``CommandLineInterface`` instance for this connection.
        (This can be replaced any time.)

        :param cli: CommandLineInterface instance.
        :param callback: Callable that takes the result of the CLI.
        """
        assert isinstance(app, Application)
        assert callback is None or callable(callback)

        self.cli = CommandLineInterface(application=app, eventloop=self.eventloop, output=self.vt100_output)
        self.callback = callback

        # Create a parser, and parser callbacks.
        cb = self.cli.create_eventloop_callbacks()
        inputstream = InputStream(cb.feed_key)

        # Input decoder for stdin. (Required when working with multibyte
        # characters, like chinese input.)
        stdin_decoder_cls = getincrementaldecoder(self.encoding)
        stdin_decoder = [stdin_decoder_cls()]  # nonlocal

        # Tell the CLI that it's running. We don't start it through the run()
        # call, but will still want _redraw() to work.
        self.cli._is_running = True

        def data_received(data):
            """ TelnetProtocolParser 'data_received' callback """
            assert isinstance(data, binary_type)

            try:
                result = stdin_decoder[0].decode(data)
                inputstream.feed(result)
            except UnicodeDecodeError:
                stdin_decoder[0] = stdin_decoder_cls()
                return ""

        def size_received(rows, columns):
            """ TelnetProtocolParser 'size_received' callback """
            self.size = Size(rows=rows, columns=columns)
            cb.terminal_size_changed()

        self.parser = TelnetProtocolParser(data_received, size_received)

    def feed(self, data):
        """
        Handler for incoming data. (Called by TelnetServer.)
        """
        assert isinstance(data, binary_type)

        self.parser.feed(data)

        # Render again.
        self.cli._redraw()

        # When a return value has been set (enter was pressed), handle command.
        if self.cli.is_returning:
            try:
                return_value = self.cli.return_value()
            except (EOFError, KeyboardInterrupt) as e:
                # Control-D or Control-C was pressed.
                logger.info("%s, closing connection.", type(e).__name__)
                self.close()
                return

            # Handle CLI command
            self._handle_command(return_value)

    def _handle_command(self, command):
        """
        Handle command. This will run in a separate thread, in order not
        to block the event loop.
        """
        logger.info("Handle command %r", command)

        def in_executor():
            self.handling_command = True
            try:
                if self.callback is not None:
                    self.callback(self, command)
            finally:
                self.server.call_from_executor(done)

        def done():
            self.handling_command = False

            # Reset state and draw again. (If the connection is still open --
            # the application could have called TelnetConnection.close()
            if not self.closed:
                self.cli.reset()
                self.cli.buffers[DEFAULT_BUFFER].reset()
                self.cli.renderer.request_absolute_cursor_position()
                self.vt100_output.flush()
                self.cli._redraw()

        self.server.run_in_executor(in_executor)

    def erase_screen(self):
        """
        Erase output screen.
        """
        self.vt100_output.erase_screen()
        self.vt100_output.cursor_goto(0, 0)
        self.vt100_output.flush()

    def send(self, data):
        """
        Send text to the client.
        """
        assert isinstance(data, text_type)

        # When data is send back to the client, we should replace the line
        # endings. (We didn't allocate a real pseudo terminal, and the telnet
        # connection is raw, so we are responsible for inserting \r.)
        self.stdout.write(data.replace("\n", "\r\n"))
        self.stdout.flush()

    def close(self):
        """
        Close the connection.
        """
        self.application.client_leaving(self)

        self.conn.close()
        self.closed = True
Ejemplo n.º 6
0
class ReplSSHServerSession(asyncssh.SSHServerSession):
    """
    SSH server session that runs a Python REPL.

    :param get_globals: callable that returns the current globals.
    :param get_locals: (optional) callable that returns the current locals.
    """
    def __init__(self, get_globals, get_locals=None):
        assert callable(get_globals)
        assert get_locals is None or callable(get_locals)

        self._chan = None

        def _globals():
            data = get_globals()
            data.setdefault('print', self._print)
            return data

        repl = PythonRepl(get_globals=_globals,
                          get_locals=get_locals or _globals)

        # Disable open-in-editor and system prompt. Because it would run and
        # display these commands on the server side, rather than in the SSH
        # client.
        repl.enable_open_in_editor = False
        repl.enable_system_bindings = False

        # PipInput object, for sending input in the CLI.
        # (This is something that we can use in the prompt_toolkit event loop,
        # but still write date in manually.)
        self._input_pipe = PipeInput()

        # Output object. Don't render to the real stdout, but write everything
        # in the SSH channel.
        class Stdout(object):
            def write(s, data):
                if self._chan is not None:
                    self._chan.write(data.replace('\n', '\r\n'))

            def flush(s):
                pass

        # Create command line interface.
        self.cli = CommandLineInterface(application=repl.create_application(),
                                        eventloop=create_asyncio_eventloop(),
                                        input=self._input_pipe,
                                        output=Vt100_Output(
                                            Stdout(), self._get_size))

        self._callbacks = self.cli.create_eventloop_callbacks()

    def _get_size(self):
        """
        Callable that returns the current `Size`, required by Vt100_Output.
        """
        if self._chan is None:
            return Size(rows=20, columns=79)
        else:
            width, height, pixwidth, pixheight = self._chan.get_terminal_size()
            return Size(rows=height, columns=width)

    def connection_made(self, chan):
        """
        Client connected, run repl in coroutine.
        """
        self._chan = chan

        # Run REPL interface.
        f = asyncio.ensure_future(self.cli.run_async())

        # Close channel when done.
        def done(_):
            chan.close()
            self._chan = None

        f.add_done_callback(done)

    def shell_requested(self):
        return True

    def terminal_size_changed(self, width, height, pixwidth, pixheight):
        """
        When the terminal size changes, report back to CLI.
        """
        self._callbacks.terminal_size_changed()

    def data_received(self, data, datatype):
        """
        When data is received, send to inputstream of the CLI and repaint.
        """
        self._input_pipe.send(data)

    def _print(self, *data, **kw):
        """
        _print(self, *data, sep=' ', end='\n', file=None)

        Alternative 'print' function that prints back into the SSH channel.
        """
        # Pop keyword-only arguments. (We cannot use the syntax from the
        # signature. Otherwise, Python2 will give a syntax error message when
        # installing.)
        sep = kw.pop('sep', ' ')
        end = kw.pop('end', '\n')
        _ = kw.pop('file', None)
        assert not kw, 'Too many keyword-only arguments'

        data = sep.join(map(str, data))
        self._chan.write(data + end)