From 553e137a13d263918c103c122424d1b0dad5efa1 Mon Sep 17 00:00:00 2001 From: Julien Duponchelle Date: Mon, 7 Nov 2016 11:16:51 +0100 Subject: [PATCH] Support for serial console for Virtual BOX and VMware using asyncio Ref #747 --- .gitignore | 1 + gns3server/compute/project.py | 13 +- .../compute/virtualbox/virtualbox_vm.py | 77 +-- gns3server/compute/vmware/vmware_vm.py | 72 +-- gns3server/schemas/virtualbox.py | 8 + gns3server/schemas/vmware.py | 8 + gns3server/utils/asyncio/serial.py | 137 ++++++ gns3server/utils/asyncio/telnet_server.py | 5 +- gns3server/utils/telnet_server.py | 442 ------------------ 9 files changed, 198 insertions(+), 565 deletions(-) create mode 100644 gns3server/utils/asyncio/serial.py delete mode 100644 gns3server/utils/telnet_server.py diff --git a/.gitignore b/.gitignore index 1cb0ea8b..093b8deb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.py[cod] +__pycache__ #py.test .cache diff --git a/gns3server/compute/project.py b/gns3server/compute/project.py index 52fca5b8..cd782a3a 100644 --- a/gns3server/compute/project.py +++ b/gns3server/compute/project.py @@ -24,7 +24,7 @@ import zipstream import zipfile import json -from uuid import UUID +from uuid import UUID, uuid4 from .port_manager import PortManager from .notification_manager import NotificationManager from ..config import Config @@ -49,10 +49,13 @@ class Project: def __init__(self, name=None, project_id=None, path=None): self._name = name - try: - UUID(project_id, version=4) - except ValueError: - raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id)) + if project_id: + try: + UUID(project_id, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(project_id)) + else: + project_id = str(uuid4()) self._id = project_id self._nodes = set() diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index 881ccb7c..32e59756 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -30,12 +30,13 @@ import asyncio import xml.etree.ElementTree as ET from gns3server.utils import parse_version -from gns3server.utils.telnet_server import TelnetServer -from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation, locked_coroutine -from .virtualbox_error import VirtualBoxError -from ..nios.nio_udp import NIOUDP -from ..adapters.ethernet_adapter import EthernetAdapter -from ..base_node import BaseNode +from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer +from gns3server.utils.asyncio.serial import asyncio_open_serial +from gns3server.utils.asyncio import locked_coroutine +from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError +from gns3server.compute.nios.nio_udp import NIOUDP +from gns3server.compute.adapters.ethernet_adapter import EthernetAdapter +from gns3server.compute.base_node import BaseNode if sys.platform.startswith('win'): import msvcrt @@ -53,12 +54,11 @@ class VirtualBoxVM(BaseNode): def __init__(self, name, node_id, project, manager, vmname, linked_clone=False, console=None, adapters=0): - super().__init__(name, node_id, project, manager, console=console, linked_clone=linked_clone) + super().__init__(name, node_id, project, manager, console=console, linked_clone=linked_clone, console_type="telnet") self._maximum_adapters = 8 self._system_properties = {} - self._telnet_server_thread = None - self._serial_pipe = None + self._telnet_server = None self._local_udp_tunnels = {} # VirtualBox settings @@ -81,6 +81,7 @@ class VirtualBoxVM(BaseNode): json = {"name": self.name, "node_id": self.id, "console": self.console, + "console_type": self.console_type, "project_id": self.project.id, "vmname": self.vmname, "headless": self.headless, @@ -243,16 +244,7 @@ class VirtualBoxVM(BaseNode): self._local_udp_tunnels[adapter_number][1], nio) - if self._console is not None: - try: - # wait for VirtualBox to create the pipe file. - if sys.platform.startswith("win"): - yield from wait_for_named_pipe_creation(self._get_pipe_name()) - else: - yield from wait_for_file_creation(self._get_pipe_name()) - except asyncio.TimeoutError: - raise VirtualBoxError('Pipe file "{}" for remote console has not been created by VirtualBox'.format(self._get_pipe_name())) - self._start_remote_console() + yield from self._start_console() if (yield from self.check_hw_virtualization()): self._hw_virtualization = True @@ -874,54 +866,21 @@ class VirtualBoxVM(BaseNode): os.makedirs(os.path.join(self.working_dir, self._vmname), exist_ok=True) - def _start_remote_console(self): + @asyncio.coroutine + def _start_console(self): """ Starts remote console support for this VM. """ - - # starts the Telnet to pipe thread - pipe_name = self._get_pipe_name() - if sys.platform.startswith("win"): - try: - self._serial_pipe = open(pipe_name, "a+b") - except OSError as e: - raise VirtualBoxError("Could not open the pipe {}: {}".format(pipe_name, e)) - try: - self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console) - except OSError as e: - raise VirtualBoxError("Unable to create Telnet server: {}".format(e)) - self._telnet_server_thread.start() - else: - try: - self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._serial_pipe.connect(pipe_name) - except OSError as e: - raise VirtualBoxError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - try: - self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._manager.port_manager.console_host, self._console) - except OSError as e: - raise VirtualBoxError("Unable to create Telnet server: {}".format(e)) - self._telnet_server_thread.start() + pipe = yield from asyncio_open_serial(self._get_pipe_name()) + server = AsyncioTelnetServer(reader=pipe, writer=pipe, binary=True, echo=True) + self._telnet_server = yield from asyncio.start_server(server.run, '127.0.0.1', self.console) def _stop_remote_console(self): """ Stops remote console support for this VM. """ - - if self._telnet_server_thread: - if self._telnet_server_thread.is_alive(): - self._telnet_server_thread.stop() - self._telnet_server_thread.join(timeout=3) - if self._telnet_server_thread.is_alive(): - log.warn("Serial pipe thread is still alive!") - self._telnet_server_thread = None - - if self._serial_pipe: - if sys.platform.startswith("win"): - win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) - else: - self._serial_pipe.close() - self._serial_pipe = None + if self._telnet_server: + self._telnet_server.close() @asyncio.coroutine def adapter_add_nio_binding(self, adapter_number, nio): diff --git a/gns3server/compute/vmware/vmware_vm.py b/gns3server/compute/vmware/vmware_vm.py index f7e581ee..d2892b22 100644 --- a/gns3server/compute/vmware/vmware_vm.py +++ b/gns3server/compute/vmware/vmware_vm.py @@ -25,18 +25,16 @@ import socket import asyncio import tempfile -from gns3server.utils.telnet_server import TelnetServer from gns3server.utils.interfaces import interfaces -from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation, locked_coroutine +from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer +from gns3server.utils.asyncio.serial import asyncio_open_serial +from gns3server.utils.asyncio import locked_coroutine from collections import OrderedDict from .vmware_error import VMwareError from ..nios.nio_udp import NIOUDP from ..adapters.ethernet_adapter import EthernetAdapter from ..base_node import BaseNode -if sys.platform.startswith('win'): - import msvcrt - import win32file import logging log = logging.getLogger(__name__) @@ -53,8 +51,7 @@ class VMwareVM(BaseNode): super().__init__(name, node_id, project, manager, console=console, linked_clone=linked_clone) self._vmx_pairs = OrderedDict() - self._telnet_server_thread = None - self._serial_pipe = None + self._telnet_server = None self._vmnets = [] self._maximum_adapters = 10 self._started = False @@ -82,6 +79,7 @@ class VMwareVM(BaseNode): json = {"name": self.name, "node_id": self.id, "console": self.console, + "console_type": self.console_type, "project_id": self.project.id, "vmx_path": self.vmx_path, "headless": self.headless, @@ -440,15 +438,7 @@ class VMwareVM(BaseNode): if nio: yield from self._add_ubridge_connection(nio, adapter_number) - # if self._console is not None: - # try: - # if sys.platform.startswith("win"): - # yield from wait_for_named_pipe_creation(self._get_pipe_name()) - # else: - # yield from wait_for_file_creation(self._get_pipe_name()) # wait for VMware to create the pipe file. - # except asyncio.TimeoutError: - # raise VMwareError('Pipe file "{}" for remote console has not been created by VMware'.format(self._get_pipe_name())) - # self._start_remote_console() + yield from self._start_console() except VMwareError: yield from self.stop() raise @@ -797,57 +787,25 @@ class VMwareVM(BaseNode): serial_port = {"serial0.present": "TRUE", "serial0.filetype": "pipe", "serial0.filename": pipe_name, - "serial0.pipe.endpoint": "server"} + "serial0.pipe.endpoint": "server", + "serial0.startconnected": "TRUE"} self._vmx_pairs.update(serial_port) - def _start_remote_console(self): + @asyncio.coroutine + def _start_console(self): """ Starts remote console support for this VM. """ - - # starts the Telnet to pipe thread - pipe_name = self._get_pipe_name() - if sys.platform.startswith("win"): - try: - self._serial_pipe = open(pipe_name, "a+b") - except OSError as e: - raise VMwareError("Could not open the pipe {}: {}".format(pipe_name, e)) - try: - self._telnet_server_thread = TelnetServer(self.name, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._manager.port_manager.console_host, self._console) - except OSError as e: - raise VMwareError("Unable to create Telnet server: {}".format(e)) - self._telnet_server_thread.start() - else: - try: - self._serial_pipe = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self._serial_pipe.connect(pipe_name) - except OSError as e: - raise VMwareError("Could not connect to the pipe {}: {}".format(pipe_name, e)) - try: - self._telnet_server_thread = TelnetServer(self.name, self._serial_pipe, self._manager.port_manager.console_host, self._console) - except OSError as e: - raise VMwareError("Unable to create Telnet server: {}".format(e)) - self._telnet_server_thread.start() + pipe = yield from asyncio_open_serial(self._get_pipe_name()) + server = AsyncioTelnetServer(reader=pipe, writer=pipe, binary=True, echo=True) + self._telnet_server = yield from asyncio.start_server(server.run, '127.0.0.1', self.console) def _stop_remote_console(self): """ Stops remote console support for this VM. """ - - if self._telnet_server_thread: - if self._telnet_server_thread.is_alive(): - self._telnet_server_thread.stop() - self._telnet_server_thread.join(timeout=3) - if self._telnet_server_thread.is_alive(): - log.warn("Serial pipe thread is still alive!") - self._telnet_server_thread = None - - if self._serial_pipe: - if sys.platform.startswith("win"): - win32file.CloseHandle(msvcrt.get_osfhandle(self._serial_pipe.fileno())) - else: - self._serial_pipe.close() - self._serial_pipe = None + if self._telnet_server: + self._telnet_server.close() @asyncio.coroutine def start_capture(self, adapter_number, output_file): diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index f154720a..5b2661b6 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -66,6 +66,10 @@ VBOX_CREATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, "ram": { "description": "Amount of RAM", "minimum": 0, @@ -152,6 +156,10 @@ VBOX_OBJECT_SCHEMA = { "maximum": 65535, "type": "integer" }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, "ram": { "description": "Amount of RAM", "minimum": 0, diff --git a/gns3server/schemas/vmware.py b/gns3server/schemas/vmware.py index 7c9bc90a..efd2cd8b 100644 --- a/gns3server/schemas/vmware.py +++ b/gns3server/schemas/vmware.py @@ -48,6 +48,10 @@ VMWARE_CREATE_SCHEMA = { "maximum": 65535, "type": "integer" }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, "headless": { "description": "Headless mode", "type": "boolean" @@ -143,6 +147,10 @@ VMWARE_OBJECT_SCHEMA = { "maximum": 65535, "type": "integer" }, + "console_type": { + "description": "Console type", + "enum": ["telnet"] + }, "linked_clone": { "description": "Whether the VM is a linked clone or not", "type": "boolean" diff --git a/gns3server/utils/asyncio/serial.py b/gns3server/utils/asyncio/serial.py new file mode 100644 index 00000000..12aa06d9 --- /dev/null +++ b/gns3server/utils/asyncio/serial.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# Copyright (C) 2016 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import asyncio + +from gns3server.utils.asyncio import wait_for_file_creation, wait_for_named_pipe_creation +from gns3server.compute.error import NodeError + +""" +This module handle connection to unix socket or Windows named pipe +""" +if sys.platform.startswith("win"): + import win32file + import win32pipe + import msvcrt + + +class SerialReaderWriterProtocol(asyncio.Protocol): + + def __init__(self): + self._output = asyncio.StreamReader() + self.transport = None + + def read(self, n=-1): + return self._output.read(n=n) + + def at_eof(self): + return self._output.at_eof() + + def write(self, data): + if self.transport: + self.transport.write(data) + + @asyncio.coroutine + def drain(self): + pass + + def connection_made(self, transport): + self.transport = transport + + def data_received(self, data): + self._output.feed_data(data) + + +class WindowsPipe: + """ + Write input and output stream to the same object + """ + + def __init__(self, path): + self._handle = open(path, "a+b") + self._pipe = msvcrt.get_osfhandle(self._handle.fileno()) + + @asyncio.coroutine + def read(self, n=-1): + (read, num_avail, num_message) = win32pipe.PeekNamedPipe(self._pipe, 0) + if num_avail > 0: + (error_code, output) = win32file.ReadFile(self._pipe, num_avail, None) + return output + yield from asyncio.sleep(0.01) + return b"" + + def at_eof(self): + return False + + def write(self, data): + win32file.WriteFile(self._pipe, data) + + @asyncio.coroutine + def drain(self): + return + + def close(self): + pass + + +@asyncio.coroutine +def _asyncio_open_serial_windows(path): + """ + Open a windows named pipe + + :returns: An IO like object + """ + + try: + yield from wait_for_named_pipe_creation(path) + except asyncio.TimeoutError: + raise NodeError('Pipe file "{}" is missing'.format(path)) + return WindowsPipe(path) + + +@asyncio.coroutine +def _asyncio_open_serial_unix(path): + """ + Open a unix socket or a windows named pipe + + :returns: An IO like object + """ + + try: + # wait for VM to create the pipe file. + yield from wait_for_file_creation(path) + except asyncio.TimeoutError: + raise NodeError('Pipe file "{}" is missing'.format(path)) + + output = SerialReaderWriterProtocol() + con = yield from asyncio.get_event_loop().create_unix_connection(lambda: output, path) + return output + + +@asyncio.coroutine +def asyncio_open_serial(path): + """ + Open a unix socket or a windows named pipe + + :returns: An IO like object + """ + + if sys.platform.startswith("win"): + return (yield from _asyncio_open_serial_windows(path)) + else: + return (yield from _asyncio_open_serial_unix(path)) diff --git a/gns3server/utils/asyncio/telnet_server.py b/gns3server/utils/asyncio/telnet_server.py index e28f622a..fd9afe1b 100644 --- a/gns3server/utils/asyncio/telnet_server.py +++ b/gns3server/utils/asyncio/telnet_server.py @@ -245,8 +245,9 @@ class AsyncioTelnetServer: log.debug("Unhandled DONT telnet command: " "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) elif iac_cmd[1] == WILL: - log.debug("Unhandled WILL telnet command: " - "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + if iac_cmd[2] not in [BINARY]: + log.debug("Unhandled WILL telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) elif iac_cmd[1] == WONT: log.debug("Unhandled WONT telnet command: " "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) diff --git a/gns3server/utils/telnet_server.py b/gns3server/utils/telnet_server.py deleted file mode 100644 index 41a9400f..00000000 --- a/gns3server/utils/telnet_server.py +++ /dev/null @@ -1,442 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2014 GNS3 Technologies Inc. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# TODO: port TelnetServer to asyncio - -import sys -import time -import threading -import socket -import select - -import logging -log = logging.getLogger(__name__) - -if sys.platform.startswith("win"): - import win32pipe - import win32file - - -class TelnetServer(threading.Thread): - - """ - Mini Telnet Server. - - :param node_name: node name - :param pipe_path: path to node pipe (UNIX socket on Linux/UNIX, Named Pipe on Windows) - :param host: server host - :param port: server port - """ - - def __init__(self, node_name, pipe_path, host, port): - - threading.Thread.__init__(self) - self._node_name = node_name - self._pipe = pipe_path - self._host = host - self._port = port - self._reader_thread = None - self._use_thread = False - self._write_lock = threading.Lock() - self._clients = {} - self._timeout = 1 - self._alive = True - - if sys.platform.startswith("win"): - # we must a thread for reading the pipe on Windows because it is a Named Pipe and it cannot be monitored by select() - self._use_thread = True - - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE): - af, socktype, proto, _, sa = res - self._server_socket = socket.socket(af, socktype, proto) - self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._server_socket.bind(sa) - self._server_socket.listen(socket.SOMAXCONN) - break - - log.info("Telnet server initialized, waiting for clients on {}:{}".format(self._host, self._port)) - - def run(self): - """ - Thread loop. - """ - - while True: - - recv_list = [self._server_socket.fileno()] - - if not self._use_thread: - recv_list.append(self._pipe.fileno()) - - for client in self._clients.values(): - if client.is_active(): - recv_list.append(client.socket().fileno()) - else: - del self._clients[client.socket().fileno()] - try: - client.socket().shutdown(socket.SHUT_RDWR) - except OSError as e: - log.warn("shutdown: {}".format(e)) - client.socket().close() - break - - try: - rlist, slist, elist = select.select(recv_list, [], [], self._timeout) - except OSError as e: - log.critical("fatal select error: {}".format(e)) - return False - - if not self._alive: - log.info("Telnet server for {} is exiting".format(self._node_name)) - return True - - for sock_fileno in rlist: - if sock_fileno == self._server_socket.fileno(): - - try: - sock, addr = self._server_socket.accept() - host, port = addr - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - log.info("new client {}:{} has connected".format(host, port)) - except OSError as e: - log.error("could not accept new client: {}".format(e)) - continue - - new_client = TelnetClient(self._node_name, sock, host, port) - self._clients[sock.fileno()] = new_client - - if self._use_thread and not self._reader_thread: - self._reader_thread = threading.Thread(target=self._reader, daemon=True) - self._reader_thread.start() - - elif not self._use_thread and sock_fileno == self._pipe.fileno(): - - data = self._read_from_pipe() - if not data: - log.warning("pipe has been closed!") - return False - for client in self._clients.values(): - try: - client.send(data) - except OSError as e: - log.debug(e) - client.deactivate() - - elif sock_fileno in self._clients: - try: - data = self._clients[sock_fileno].socket_recv() - - if not data: - continue - - # For some reason, windows likes to send "cr/lf" when you send a "cr". - # Strip that so we don't get a double prompt. - data = data.replace(b"\r\n", b"\n") - - self._write_to_pipe(data) - except Exception as msg: - log.info(msg) - self._clients[sock_fileno].deactivate() - - def _write_to_pipe(self, data): - """ - Writes data to the pipe. - - :param data: data to write - """ - - if sys.platform.startswith('win'): - win32file.WriteFile(self._pipe, data) - else: - self._pipe.sendall(data) - - def _read_from_pipe(self): - """ - Reads data from the pipe. - - :returns: data - """ - - if sys.platform.startswith('win'): - (read, num_avail, num_message) = win32pipe.PeekNamedPipe(self._pipe, 0) - if num_avail > 0: - (error_code, output) = win32file.ReadFile(self._pipe, num_avail, None) - return output - return b"" - else: - return self._pipe.recv(1024) - - def _reader(self): - """ - Loops forever and copy everything from the pipe to the socket. - """ - - log.debug("reader thread has started") - while self._alive: - try: - data = self._read_from_pipe() - if not data and not sys.platform.startswith('win'): - log.debug("pipe has been closed! (no data)") - break - self._write_lock.acquire() - try: - for client in self._clients.values(): - client.send(data) - finally: - self._write_lock.release() - if sys.platform.startswith('win'): - # sleep every 10 ms - time.sleep(0.01) - except Exception as e: - log.debug("pipe has been closed! {}".format(e)) - break - log.debug("reader thread exited") - self.stop() - - def stop(self): - """ - Stops the server. - """ - - if self._alive: - self._alive = False - - for client in self._clients.values(): - client.socket().close() - client.deactivate() - -# Mostly from https://code.google.com/p/miniboa/source/browse/trunk/miniboa/telnet.py - -# Telnet Commands -SE = 240 # End of sub-negotiation parameters -NOP = 241 # No operation -DATMK = 242 # Data stream portion of a sync. -BREAK = 243 # NVT Character BRK -IP = 244 # Interrupt Process -AO = 245 # Abort Output -AYT = 246 # Are you there -EC = 247 # Erase Character -EL = 248 # Erase Line -GA = 249 # The Go Ahead Signal -SB = 250 # Sub-option to follow -WILL = 251 # Will; request or confirm option begin -WONT = 252 # Wont; deny option request -DO = 253 # Do = Request or confirm remote option -DONT = 254 # Don't = Demand or confirm option halt -IAC = 255 # Interpret as Command -SEND = 1 # Sub-process negotiation SEND command -IS = 0 # Sub-process negotiation IS command - -# Telnet Options -BINARY = 0 # Transmit Binary -ECHO = 1 # Echo characters back to sender -RECON = 2 # Reconnection -SGA = 3 # Suppress Go-Ahead -TMARK = 6 # Timing Mark -TTYPE = 24 # Terminal Type -NAWS = 31 # Negotiate About Window Size -LINEMO = 34 # Line Mode - - -class TelnetClient(object): - - """ - Represents a Telnet client connection. - - :param node_name: Node name - :param sock: socket connection - :param host: IP of the Telnet client - :param port: port of the Telnet client - """ - - def __init__(self, node_name, sock, host, port): - - self._active = True - self._sock = sock - self._host = host - self._port = port - - sock.send(bytes([IAC, WILL, ECHO, - IAC, WILL, SGA, - IAC, WILL, BINARY, - IAC, DO, BINARY])) - - welcome_msg = "{} console is now available... Press RETURN to get started.\r\n".format(node_name) - sock.send(welcome_msg.encode('utf-8')) - - def is_active(self): - """ - Returns either the client is active or not. - - :return: boolean - """ - - return self._active - - def socket(self): - """ - Returns the socket for this Telnet client. - - :returns: socket instance. - """ - - return self._sock - - def send(self, data): - """ - Sends data to the remote end. - - :param data: data to send - """ - - try: - self._sock.send(data) - except OSError as e: - self._active = False - raise Exception("Socket send: {}".format(e)) - - def deactivate(self): - """ - Sets the client to disconnect on the next server poll. - """ - - self._active = False - - def socket_recv(self): - """ - Called by Telnet Server when data is ready. - """ - - try: - buf = self._sock.recv(1024) - except BlockingIOError: - return None - except ConnectionResetError: - buf = b'' - - # is the connection closed? - if not buf: - raise Exception("connection closed by {}:{}".format(self._host, self._port)) - - # Process and remove any telnet commands from the buffer - if IAC in buf: - buf = self._IAC_parser(buf) - - return buf - - def _read_block(self, bufsize): - """ - Reads a block for data from the socket. - - :param bufsize: size of the buffer - :returns: data read - """ - buf = self._sock.recv(1024, socket.MSG_WAITALL) - # If we don't get everything we were looking for then the - # client probably disconnected. - if len(buf) < bufsize: - raise Exception("connection closed by {}:{}".format(self._host, self._port)) - return buf - - def _IAC_parser(self, buf): - """ - Processes and removes any Telnet commands from the buffer. - - :param buf: buffer - :returns: buffer minus Telnet commands - """ - - skip_to = 0 - while self._active: - # Locate an IAC to process - iac_loc = buf.find(IAC, skip_to) - if iac_loc < 0: - break - - # Get the TELNET command - iac_cmd = bytearray([IAC]) - try: - iac_cmd.append(buf[iac_loc + 1]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 1]) - - # Is this just a 2-byte TELNET command? - if iac_cmd[1] not in [WILL, WONT, DO, DONT]: - if iac_cmd[1] == AYT: - log.debug("Telnet server received Are-You-There (AYT)") - self._sock.send(b'\r\nYour Are-You-There received. I am here.\r\n') - elif iac_cmd[1] == IAC: - # It's data, not an IAC - iac_cmd.pop() - # This prevents the 0xff from being - # interrupted as yet another IAC - skip_to = iac_loc + 1 - log.debug("Received IAC IAC") - elif iac_cmd[1] == NOP: - pass - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x}".format(*iac_cmd)) - - # This must be a 3-byte TELNET command - else: - try: - iac_cmd.append(buf[iac_loc + 2]) - except IndexError: - buf.extend(self._read_block(1)) - iac_cmd.append(buf[iac_loc + 2]) - # We do ECHO, SGA, and BINARY. Period. - if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]: - self._sock.send(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) - else: - log.debug("Unhandled telnet command: " - "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) - - # Remove the entire TELNET command from the buffer - buf = buf.replace(iac_cmd, b'', 1) - - # Return the new copy of the buffer, minus telnet commands - return buf - -if __name__ == '__main__': - - logging.basicConfig(level=logging.INFO) - if sys.platform.startswith('win'): - import msvcrt - pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_4.7.1' - pipe = open(pipe_name, 'a+b') - telnet_server = TelnetServer("VBOX", msvcrt.get_osfhandle(pipe.fileno()), "127.0.0.1", 3900) - else: - pipe_name = "/tmp/pipe_test" - try: - unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - unix_socket.connect(pipe_name) - except OSError as e: - print("Could not connect to UNIX socket {}: {}".format(pipe_name, e)) - sys.exit(False) - telnet_server = TelnetServer("VBOX", unix_socket, "127.0.0.1", 3900) - - telnet_server.setDaemon(True) - telnet_server.start() - try: - telnet_server.join() - except KeyboardInterrupt: - telnet_server.stop() - telnet_server.join(timeout=3)