1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2024-12-27 08:35:22 +00:00
webvirtcloud/console/sshtunnels.py
2022-11-02 08:54:35 +03:00

217 lines
5.8 KiB
Python

# Copyright (C) 2014, 2015 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
import functools
import logging as log
import os
import queue
import signal
import socket
import threading
class _TunnelScheduler(object):
"""
If the user is using Spice + SSH URI + no SSH keys, we need to
serialize connection opening otherwise ssh-askpass gets all angry.
This handles the locking and scheduling.
It's only instantiated once for the whole app, because we serialize
independent of connection, vm, etc.
"""
def __init__(self):
self._thread = None
self._queue = queue.Queue()
self._lock = threading.Lock()
def _handle_queue(self):
while True:
(
lock_cb,
cb,
args,
) = self._queue.get()
lock_cb()
cb(*args)
def schedule(self, lock_cb, cb, *args):
if not self._thread:
self._thread = threading.Thread(
name="Tunnel thread", target=self._handle_queue, args=()
)
self._thread.daemon = True
if not self._thread.is_alive():
self._thread.start()
self._queue.put((lock_cb, cb, args))
def lock(self):
self._lock.acquire()
def unlock(self):
self._lock.release()
_tunnel_scheduler = _TunnelScheduler()
class _Tunnel(object):
def __init__(self):
self._pid = None
self._closed = False
self._errfd = None
def close(self):
if self._closed:
return
self._closed = True
log.debug(
"Close tunnel PID=%s ERRFD=%s",
self._pid,
self._errfd and self._errfd.fileno() or None,
)
# Since this is a socket object, the file descriptor is closed
# when it's garbage collected.
self._errfd = None
if self._pid:
os.kill(self._pid, signal.SIGKILL)
os.waitpid(self._pid, 0)
self._pid = None
def get_err_output(self):
errout = ""
while True:
try:
new = self._errfd.recv(1024)
except Exception:
break
if not new:
break
errout += new.decode()
return errout
def open(self, argv, sshfd):
if self._closed:
return
errfds = socket.socketpair()
pid = os.fork()
if pid == 0:
errfds[0].close()
os.dup2(sshfd.fileno(), 0)
os.dup2(sshfd.fileno(), 1)
os.dup2(errfds[1].fileno(), 2)
os.execlp(*argv)
os._exit(1) # pylint: disable=protected-access
sshfd.close()
errfds[1].close()
self._errfd = errfds[0]
self._errfd.setblocking(0)
log.debug("Opened tunnel PID=%d ERRFD=%d", pid, self._errfd.fileno())
self._pid = pid
def _make_ssh_command(connhost, connuser, connport, gaddr, gport, gsocket):
# Build SSH cmd
argv = ["ssh", "ssh"]
if connport:
argv += ["-p", str(connport)]
if connuser:
argv += ["-l", connuser]
argv += [connhost]
# Build 'nc' command run on the remote host
#
# This ugly thing is a shell script to detect availability of
# the -q option for 'nc': debian and suse based distros need this
# flag to ensure the remote nc will exit on EOF, so it will go away
# when we close the VNC tunnel. If it doesn't go away, subsequent
# VNC connection attempts will hang.
#
# Fedora's 'nc' doesn't have this option, and apparently defaults
# to the desired behavior.
#
if gsocket:
nc_params = "-U %s" % gsocket
else:
nc_params = "%s %s" % (gaddr, gport)
nc_cmd = (
"""nc -q 2>&1 | grep "requires an argument" >/dev/null;"""
"""if [ $? -eq 0 ] ; then"""
""" CMD="nc -q 0 %(nc_params)s";"""
"""else"""
""" CMD="nc %(nc_params)s";"""
"""fi;"""
"""eval "$CMD";""" % {"nc_params": nc_params}
)
argv.append("sh -c")
argv.append("'%s'" % nc_cmd)
argv_str = functools.reduce(lambda x, y: x + " " + y, argv[1:])
log.debug("Pre-generated ssh command for info: %s", argv_str)
return argv
class SSHTunnels(object):
def __init__(self, connhost, connuser, connport, gaddr, gport, gsocket):
self._tunnels = []
self._sshcommand = _make_ssh_command(
connhost, connuser, connport, gaddr, gport, gsocket
)
self._locked = False
def open_new(self):
t = _Tunnel()
self._tunnels.append(t)
# socket FDs are closed when the object is garbage collected. This
# can close an FD behind spice/vnc's back which causes crashes.
#
# Dup a bare FD for the viewer side of things, but keep the high
# level socket object for the SSH side, since it simplifies things
# in that area.
viewerfd, sshfd = socket.socketpair()
_tunnel_scheduler.schedule(self._lock, t.open, self._sshcommand, sshfd)
retfd = os.dup(viewerfd.fileno())
log.debug("Generated tunnel fd=%s for viewer", retfd)
return retfd
def close_all(self):
for l in self._tunnels:
l.close()
self._tunnels = []
self.unlock()
def get_err_output(self):
errstrings = []
for l in self._tunnels:
e = l.get_err_output().strip()
if e and e not in errstrings:
errstrings.append(e)
return "\n".join(errstrings)
def _lock(self):
_tunnel_scheduler.lock()
self._locked = True
def unlock(self, *args, **kwargs):
if self._locked:
_tunnel_scheduler.unlock(*args, **kwargs)
self._locked = False