diff --git a/gns3server/modules/iou/iou_device.py b/gns3server/modules/iou/iou_device.py index 287e0c11..cef5c8c4 100644 --- a/gns3server/modules/iou/iou_device.py +++ b/gns3server/modules/iou/iou_device.py @@ -521,7 +521,7 @@ class IOUDevice(object): try: output = subprocess.check_output(["ldd", self._path]) - except (subprocess.SubprocessError, FileNotFoundError) as e: + except (FileNotFoundError, subprocess.CalledProcessError) as e: log.warn("could not determine the shared library dependencies for {}: {}".format(self._path, e)) return @@ -761,7 +761,7 @@ class IOUDevice(object): command.extend(["-l"]) else: raise IOUError("layer 1 keepalive messages are not supported by {}".format(os.path.basename(self._path))) - except OSError as e: + except (OSError, subprocess.CalledProcessError) as e: log.warn("could not determine if layer 1 keepalive messages are supported by {}: {}".format(os.path.basename(self._path), e)) def _build_command(self): diff --git a/gns3server/modules/virtualbox/pipe_proxy.py b/gns3server/modules/virtualbox/pipe_proxy.py new file mode 100644 index 00000000..5a5b8710 --- /dev/null +++ b/gns3server/modules/virtualbox/pipe_proxy.py @@ -0,0 +1,477 @@ +# -*- 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 . + +# Parts of this code have been taken from Pyserial project (http://pyserial.sourceforge.net/) under Python license + +import sys +import time +import threading +import socket +import select + +if sys.platform.startswith("win"): + import win32pipe + import win32file + + +class PipeProxy(threading.Thread): + + def __init__(self, name, pipe, host, port): + self.devname = name + self.pipe = pipe + self.host = host + self.port = port + self.server = None + self.reader_thread = None + self.use_thread = False + self._write_lock = threading.Lock() + self.clients = {} + self.timeout = 0.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 + + try: + if self.host.__contains__(':'): + # IPv6 address support + self.server = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + else: + self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server.bind((self.host, int(self.port))) + self.server.listen(5) + except socket.error as msg: + self.error("unable to create the socket server %s" % msg) + return + + threading.Thread.__init__(self) + self.debug("initialized, waiting for clients on %s:%i..." % (self.host, self.port)) + + def error(self, msg): + + sys.stderr.write("ERROR -> %s PIPE PROXY: %s\n" % (self.devname, msg)) + + def debug(self, msg): + + sys.stdout.write("INFO -> %s PIPE PROXY: %s\n" % (self.devname, msg)) + + def run(self): + + while True: + + recv_list = [self.server.fileno()] + + if not self.use_thread: + recv_list.append(self.pipe.fileno()) + + for client in self.clients.values(): + if client.active: + recv_list.append(client.fileno) + else: + self.debug("lost client %s" % client.addrport()) + try: + client.sock.close() + except: + pass + del self.clients[client.fileno] + + try: + rlist, slist, elist = select.select(recv_list, [], [], self.timeout) + except select.error as err: + self.error("fatal select error %d:%s" % (err[0], err[1])) + return False + + if not self.alive: + self.debug('Exiting ...') + return True + + for sock_fileno in rlist: + if sock_fileno == self.server.fileno(): + + try: + sock, addr = self.server.accept() + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + self.debug("new client %s:%s" % (addr[0], addr[1])) + except socket.error as err: + self.error("accept error %d:%s" % (err[0], err[1])) + continue + + new_client = TelnetClient(sock, addr) + self.clients[new_client.fileno] = new_client + welcome_msg = "%s console is now available ... Press RETURN to get started.\r\n" % self.devname + sock.send(welcome_msg.encode('utf-8')) + + if self.use_thread and not self.reader_thread: + self.reader_thread = threading.Thread(target=self.reader) + self.reader_thread.setDaemon(True) + self.reader_thread.setName('pipe->socket') + self.reader_thread.start() + + elif not self.use_thread and sock_fileno == self.pipe.fileno(): + + data = self.read_from_pipe() + if not data: + self.debug("pipe has been closed!") + return False + for client in self.clients.values(): + try: + client.send(data) + except: + self.debug(msg) + client.deactivate() + elif sock_fileno in self.clients: + try: + data = self.clients[sock_fileno].socket_recv() + + # 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 = string.replace(data, chr(13) + chr(10), chr(13)) + data = data.replace(bytearray([13, 10]), bytes(13)) + + self.write_to_pipe(data) + except Exception as msg: + self.debug(msg) + self.clients[sock_fileno].deactivate() + + def write_to_pipe(self, data): + + if sys.platform.startswith('win'): + win32file.WriteFile(self.pipe, data) + else: + self.pipe.sendall(data) + + def read_from_pipe(self): + + 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 "" + else: + return self.pipe.recv(1024) + + def reader(self): + """loop forever and copy pipe->socket""" + + self.debug("reader thread started") + while self.alive: + try: + data = self.read_from_pipe() + if not data and not sys.platform.startswith('win'): + self.debug("pipe has been closed!") + 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: + self.debug("pipe has been closed!") + break + self.debug("reader thread exited") + self.stop() + + def stop(self): + """Stop copying""" + + if self.alive: + self.alive = False + for client in self.clients.values(): + client.sock.close() + client.deactivate() + +# telnet protocol characters +IAC = 255 # Interpret As Command +DONT = 254 +DO = 253 +WONT = 252 +WILL = 251 +IAC_DOUBLED = [IAC, IAC] + +SE = 240 # Subnegotiation End +NOP = 241 # No Operation +DM = 242 # Data Mark +BRK = 243 # Break +IP = 244 # Interrupt process +AO = 245 # Abort output +AYT = 246 # Are You There +EC = 247 # Erase Character +EL = 248 # Erase Line +GA = 249 # Go Ahead +SB = 250 # Subnegotiation Begin + +# selected telnet options +ECHO = 1 # echo +SGA = 3 # suppress go ahead +LINEMODE = 34 # line mode +TERMTYPE = 24 # terminal type + +# Telnet filter states +M_NORMAL = 0 +M_IAC_SEEN = 1 +M_NEGOTIATE = 2 + +# TelnetOption and TelnetSubnegotiation states +REQUESTED = 'REQUESTED' +ACTIVE = 'ACTIVE' +INACTIVE = 'INACTIVE' +REALLY_INACTIVE = 'REALLY_INACTIVE' + +class TelnetOption(object): + """Manage a single telnet option, keeps track of DO/DONT WILL/WONT.""" + + def __init__(self, connection, name, option, send_yes, send_no, ack_yes, ack_no, initial_state, activation_callback=None): + """Init option. + :param connection: connection used to transmit answers + :param name: a readable name for debug outputs + :param send_yes: what to send when option is to be enabled. + :param send_no: what to send when option is to be disabled. + :param ack_yes: what to expect when remote agrees on option. + :param ack_no: what to expect when remote disagrees on option. + :param initial_state: options initialized with REQUESTED are tried to + be enabled on startup. use INACTIVE for all others. + """ + self.connection = connection + self.name = name + self.option = option + self.send_yes = send_yes + self.send_no = send_no + self.ack_yes = ack_yes + self.ack_no = ack_no + self.state = initial_state + self.active = False + self.activation_callback = activation_callback + + def __repr__(self): + """String for debug outputs""" + return "%s:%s(%s)" % (self.name, self.active, self.state) + + def process_incoming(self, command): + """A DO/DONT/WILL/WONT was received for this option, update state and + answer when needed.""" + if command == self.ack_yes: + if self.state is REQUESTED: + self.state = ACTIVE + self.active = True + if self.activation_callback is not None: + self.activation_callback() + elif self.state is ACTIVE: + pass + elif self.state is INACTIVE: + self.state = ACTIVE + self.connection.telnetSendOption(self.send_yes, self.option) + self.active = True + if self.activation_callback is not None: + self.activation_callback() + elif self.state is REALLY_INACTIVE: + self.connection.telnetSendOption(self.send_no, self.option) + else: + raise ValueError('option in illegal state %r' % self) + elif command == self.ack_no: + if self.state is REQUESTED: + self.state = INACTIVE + self.active = False + elif self.state is ACTIVE: + self.state = INACTIVE + self.connection.telnetSendOption(self.send_no, self.option) + self.active = False + elif self.state is INACTIVE: + pass + elif self.state is REALLY_INACTIVE: + pass + else: + raise ValueError('option in illegal state %r' % self) + +class TelnetClient(object): + + """ + Represents a client connection via Telnet. + + First argument is the socket discovered by the Telnet Server. + Second argument is the tuple (ip address, port number). + """ + + def __init__(self, sock, addr_tup): + self.active = True # Turns False when the connection is lost + self.sock = sock # The connection's socket + self.fileno = sock.fileno() # The socket's file descriptor + self.address = addr_tup[0] # The client's remote TCP/IP address + self.port = addr_tup[1] # The client's remote port + + # filter state machine + self.mode = M_NORMAL + self.suboption = None + self.telnet_command = None + + # all supported telnet options + self._telnet_options = [ + TelnetOption(self, 'ECHO', ECHO, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'we-SGA', SGA, WILL, WONT, DO, DONT, REQUESTED), + TelnetOption(self, 'they-SGA', SGA, DO, DONT, WILL, WONT, INACTIVE), + TelnetOption(self, 'LINEMODE', LINEMODE, DONT, DONT, WILL, WONT, REQUESTED), + TelnetOption(self, 'TERMTYPE', TERMTYPE, DO, DONT, WILL, WONT, REQUESTED), + ] + + for option in self._telnet_options: + if option.state is REQUESTED: + self.telnetSendOption(option.send_yes, option.option) + + def telnetSendOption(self, action, option): + """Send DO, DONT, WILL, WONT.""" + self.sock.sendall(bytes([IAC, action, option])) + + def escape(self, data): + """ All outgoing data has to be properly escaped, so that no IAC character + in the data stream messes up the Telnet state machine in the server. + """ + for byte in data: + if byte == IAC: + yield IAC + yield IAC + else: + yield byte + + def filter(self, data): + """ handle a bunch of incoming bytes. this is a generator. it will yield + all characters not of interest for Telnet + """ + for byte in data: + if self.mode == M_NORMAL: + # interpret as command or as data + if byte == IAC: + self.mode = M_IAC_SEEN + else: + # store data in sub option buffer or pass it to our + # consumer depending on state + if self.suboption is not None: + self.suboption.append(byte) + else: + yield byte + elif self.mode == M_IAC_SEEN: + if byte == IAC: + # interpret as command doubled -> insert character + # itself + if self.suboption is not None: + self.suboption.append(byte) + else: + yield byte + self.mode = M_NORMAL + elif byte == SB: + # sub option start + self.suboption = bytearray() + self.mode = M_NORMAL + elif byte == SE: + # sub option end -> process it now + #self._telnetProcessSubnegotiation(bytes(self.suboption)) + self.suboption = None + self.mode = M_NORMAL + elif byte in (DO, DONT, WILL, WONT): + # negotiation + self.telnet_command = byte + self.mode = M_NEGOTIATE + else: + # other telnet commands are ignored! + self.mode = M_NORMAL + elif self.mode == M_NEGOTIATE: # DO, DONT, WILL, WONT was received, option now following + self._telnetNegotiateOption(self.telnet_command, byte) + self.mode = M_NORMAL + + def _telnetNegotiateOption(self, command, option): + """Process incoming DO, DONT, WILL, WONT.""" + # check our registered telnet options and forward command to them + # they know themselves if they have to answer or not + known = False + for item in self._telnet_options: + # can have more than one match! as some options are duplicated for + # 'us' and 'them' + if item.option == option: + item.process_incoming(command) + known = True + if not known: + # handle unknown options + # only answer to positive requests and deny them + if command == WILL or command == DO: + self.telnetSendOption((command == WILL and DONT or WONT), option) + + def send(self, data): + """ + Send data to the distant end. + """ + + try: + self.sock.sendall(bytes(self.escape(data))) + except socket.error as ex: + self.active = False + raise Exception("socket.sendall() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport())) + + def deactivate(self): + """ + Set the client to disconnect on the next server poll. + """ + self.active = False + + def addrport(self): + """ + Return the DE's IP address and port number as a string. + """ + return "%s:%s" % (self.address, self.port) + + def socket_recv(self): + """ + Called by TelnetServer when recv data is ready. + """ + try: + data = self.sock.recv(4096) + except socket.error as ex: + raise Exception("socket.recv() error '%d:%s' from %s" % (ex[0], ex[1], self.addrport())) + + ## Did they close the connection? + size = len(data) + if size == 0: + raise Exception("connection closed by %s" % self.addrport()) + + return bytes(self.filter(data)) + +if __name__ == '__main__': + + if sys.platform.startswith('win'): + import msvcrt + pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_3.8.2' + pipe = open(pipe_name, 'a+b') + pipe_proxy = PipeProxy("VBOX", msvcrt.get_osfhandle(pipe.fileno()), '127.0.0.1', 3900) + else: + try: + unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + #unix_socket.settimeout(0.1) + unix_socket.connect("/tmp/pipe_test") + except socket.error as err: + print("Socket error -> %d:%s" % (err[0], err[1])) + sys.exit(False) + pipe_proxy = PipeProxy('VBOX', unix_socket, '127.0.0.1', 3900) + + pipe_proxy.setDaemon(True) + pipe_proxy.start() + pipe.proxy.stop() + pipe_proxy.join() diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 76a6532a..3614872d 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -25,14 +25,21 @@ import shutil import tempfile import re import time +import socket +from .pipe_proxy import PipeProxy from .virtualbox_error import VirtualBoxError from .adapters.ethernet_adapter import EthernetAdapter from ..attic import find_unused_port +if sys.platform.startswith('win'): + import msvcrt + import win32file + import logging log = logging.getLogger(__name__) + class VirtualBoxVM(object): """ VirtualBox VM implementation. @@ -91,6 +98,10 @@ class VirtualBoxVM(object): self._console_start_port_range = console_start_port_range self._console_end_port_range = console_end_port_range + # Telnet to pipe mini-server + self._serial_pipe_thread = None + self._serial_pipe = None + # VirtualBox API variables self._machine = None self._session = None @@ -464,6 +475,26 @@ class VirtualBoxVM(object): except Exception: pass + # 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)) + self._serial_pipe_thread = PipeProxy(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console) + self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_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)) + self._serial_pipe_thread = PipeProxy(self._vmname, self._serial_pipe, self._host, self._console) + self._serial_pipe_thread.setDaemon(True) + self._serial_pipe_thread.start() + def stop(self): """ Stops this VirtualBox VM. @@ -489,6 +520,18 @@ class VirtualBoxVM(object): # This can happen, if user manually kills VBox VM. log.warn("could not stop VM for {}: {}".format(self._vmname, e)) return + finally: + if self._serial_pipe_thread: + self._serial_pipe_thread.stop() + self._serial_pipe_thread.join() + self._serial_pipe_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 def suspend(self): """ @@ -835,19 +878,22 @@ class VirtualBoxVM(object): time.sleep(0.75) continue - def _set_console_options(self): + def _get_pipe_name(self): - log.info("setting console options for {}".format(self.vmname)) - - self._lock_machine() - - # pick a pipe name p = re.compile('\s+', re.UNICODE) pipe_name = p.sub("_", self._vmname) if sys.platform.startswith('win'): pipe_name = r"\\.\pipe\VBOX\{}".format(pipe_name) else: pipe_name = os.path.join(tempfile.gettempdir(), "pipe_{}".format(pipe_name)) + return pipe_name + + def _set_console_options(self): + + log.info("setting console options for {}".format(self.vmname)) + + self._lock_machine() + pipe_name = self._get_pipe_name() try: serial_port = self._session.machine.getSerialPort(0) diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 5569c5d5..3b0fdb2c 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -348,7 +348,7 @@ class VPCSDevice(object): raise VPCSError("VPCS executable version must be >= 0.5b1") else: raise VPCSError("Could not determine the VPCS version for {}".format(self._path)) - except OSError as e: + except (OSError, subprocess.CalledProcessError) as e: raise VPCSError("Error while looking for the VPCS version: {}".format(e)) def start(self):