mirror of
https://github.com/GNS3/gns3-server
synced 2024-11-28 11:18:11 +00:00
New Telnet server for VirtualBox.
This commit is contained in:
parent
4d23c5917c
commit
393a312e7e
@ -347,8 +347,13 @@ class Dynamips(IModule):
|
||||
# for local server
|
||||
new_working_dir = request.pop("working_dir")
|
||||
|
||||
self._working_dir = new_working_dir
|
||||
try:
|
||||
self._hypervisor_manager.working_dir = new_working_dir
|
||||
except DynamipsError as e:
|
||||
log.error("could not change working directory: {}".format(e))
|
||||
return
|
||||
|
||||
self._working_dir = new_working_dir
|
||||
|
||||
# apply settings to the hypervisor manager
|
||||
for name, value in request.items():
|
||||
|
@ -1,476 +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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
# 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 = data.replace(b"\r\n", b"\n")
|
||||
|
||||
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()
|
444
gns3server/modules/virtualbox/telnet_server.py
Normal file
444
gns3server/modules/virtualbox/telnet_server.py
Normal file
@ -0,0 +1,444 @@
|
||||
# -*- 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
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 vm_name: Virtual machine name
|
||||
:param pipe_path: path to VM pipe (UNIX socket on Linux/UNIX, Named Pipe on Windows)
|
||||
:param host: server host
|
||||
:param port: server port
|
||||
"""
|
||||
|
||||
def __init__(self, vm_name, pipe_path, host, port):
|
||||
|
||||
self._vm_name = vm_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
|
||||
|
||||
try:
|
||||
if ":" in self._host:
|
||||
# IPv6 address support
|
||||
self._server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self._server_socket.bind((self._host, self._port))
|
||||
self._server_socket.listen(socket.SOMAXCONN)
|
||||
except OSError as e:
|
||||
log.critical("unable to create a server socket: {}".format(e))
|
||||
return
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
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._vm_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._vm_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 ""
|
||||
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!")
|
||||
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:
|
||||
log.debug("pipe has been closed!")
|
||||
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 vm_name: VM name
|
||||
:param sock: socket connection
|
||||
:param host: IP of the Telnet client
|
||||
:param port: port of the Telnet client
|
||||
"""
|
||||
|
||||
def __init__(self, vm_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(vm_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, socket.MSG_DONTWAIT)
|
||||
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__':
|
||||
|
||||
log.setLevel(logging.INFO)
|
||||
if sys.platform.startswith('win'):
|
||||
import msvcrt
|
||||
pipe_name = r'\\.\pipe\VBOX\Linux_Microcore_3.8.2'
|
||||
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)
|
@ -32,7 +32,7 @@ import time
|
||||
from .virtualbox_error import VirtualBoxError
|
||||
from .adapters.ethernet_adapter import EthernetAdapter
|
||||
from ..attic import find_unused_port
|
||||
from .pipe_proxy import PipeProxy
|
||||
from .telnet_server import TelnetServer
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
import msvcrt
|
||||
@ -96,7 +96,7 @@ class VirtualBoxVM(object):
|
||||
self._console_start_port_range = console_start_port_range
|
||||
self._console_end_port_range = console_end_port_range
|
||||
|
||||
self._serial_pipe_thread = None
|
||||
self._telnet_server_thread = None
|
||||
self._serial_pipe = None
|
||||
|
||||
# VirtualBox settings
|
||||
@ -626,6 +626,7 @@ class VirtualBoxVM(object):
|
||||
args = [self._vmname, "--nictype{}".format(adapter_id + 1), vbox_adapter_type]
|
||||
self._execute("modifyvm", args)
|
||||
|
||||
self._modify_vm("--nictrace{} off".format(adapter_id + 1))
|
||||
nio = self._ethernet_adapters[adapter_id].get_nio(0)
|
||||
if nio:
|
||||
log.debug("setting UDP params on adapter {}".format(adapter_id))
|
||||
@ -639,8 +640,6 @@ class VirtualBoxVM(object):
|
||||
if nio.capturing:
|
||||
self._modify_vm("--nictrace{} on".format(adapter_id + 1))
|
||||
self._modify_vm("--nictracefile{} {}".format(adapter_id + 1, nio.pcap_output_file))
|
||||
else:
|
||||
self._modify_vm("--nictrace{} off".format(adapter_id + 1))
|
||||
else:
|
||||
# shutting down unused adapters...
|
||||
self._modify_vm("--nic{} null".format(adapter_id + 1))
|
||||
@ -681,30 +680,28 @@ class VirtualBoxVM(object):
|
||||
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()
|
||||
self._telnet_server_thread = TelnetServer(self._vmname, msvcrt.get_osfhandle(self._serial_pipe.fileno()), self._host, self._console)
|
||||
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))
|
||||
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()
|
||||
self._telnet_server_thread = TelnetServer(self._vmname, self._serial_pipe, self._host, self._console)
|
||||
self._telnet_server_thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops this VirtualBox VM.
|
||||
"""
|
||||
|
||||
if self._serial_pipe_thread:
|
||||
self._serial_pipe_thread.stop()
|
||||
self._serial_pipe_thread.join(1)
|
||||
if self._serial_pipe_thread.isAlive():
|
||||
if self._telnet_server_thread:
|
||||
self._telnet_server_thread.stop()
|
||||
self._telnet_server_thread.join(timeout=3)
|
||||
if self._telnet_server_thread.isAlive():
|
||||
log.warn("Serial pire thread is still alive!")
|
||||
self._serial_pipe_thread = None
|
||||
self._telnet_server_thread = None
|
||||
|
||||
if self._serial_pipe:
|
||||
if sys.platform.startswith('win'):
|
||||
|
Loading…
Reference in New Issue
Block a user