# Python X2Go (Client) API Documentation (v0.6.1.3)

## x2go.forward

# Source code for x2go.forward

    # Copyright (C) 2010-2020 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
    #
    # Python X2Go is free software; you can redistribute it and/or modify
    # it under the terms of the GNU Affero General Public License as published by
    # the Free Software Foundation; either version 3 of the License, or
    # (at your option) any later version.
    #
    # Python X2Go 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 Affero General Public License for more details.
    #
    # You should have received a copy of the GNU Affero General Public License
    # along with this program; if not, write to the
    # Free Software Foundation, Inc.,
    # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

    """\
    Python Gevent based port forwarding server (openssh -L option) for the
    proxying of graphical X2Go elements.

    """
    __NAME__ = "x2gofwtunnel-pylib"

    __package__ = 'x2go'
    __name__    = 'x2go.forward'

    # modules
    import copy

    # gevent/greenlet
    import gevent
    from gevent import select, socket
    from gevent.server import StreamServer

    # Python X2Go modules
    from . import log
    from .defaults import X2GOCLIENT_OS as _X2GOCLIENT_OS

    class X2GoFwServer(StreamServer):
        """\
        :class:`x2go.forward.X2GoFwServer` implements a gevent's StreamServer based Paramiko/SSH port
        forwarding server.

        An :class:`x2go.forward.X2GoFwServer` class object is used to tunnel graphical trafic
        through an external proxy command launched by a ``X2GoProxy*`` backend.

        """
        def __init__ (self, listener,
                      remote_host, remote_port,
                      ssh_transport, session_instance=None, session_name=None,
                      subsystem=None, logger=None, loglevel=log.loglevel_DEFAULT,):
            """\
            :param listener: listen on TCP/IP socket ``(<IP>, <Port>)``
            :type listener: ``tuple``
            :param remote_host: hostname or IP of remote host (in case of X2Go mostly 127.0.0.1)
            :type remote_host: ``str``
            :param remote_port: port of remote host
            :type remote_port: ``int``
            :param ssh_transport: a valid Paramiko/SSH transport object
            :type ssh_transport: ``obj``
            :param session_instance: the complete :class:`x2go.session.X2GoSession` instance of the X2Go session this port forwarding server belongs to.
                Note: for new :class:`x2go.session.X2GoSession` instances the object has the session name not yet set(!!!)
            :type session_instance: ``obj``
            :param session_name: the session name of the X2Go session this port forwarding server belongs to
            :type session_name: ``str``
            :param logger: you can pass an :class:`x2go.log.X2GoLogger` object to the
                :class:`x2go.forward.X2GoFwServer` constructor
            :type logger: ``obj``
            :param loglevel: if no :class:`x2go.log.X2GoLogger` object has been supplied a new one will be
                constructed with the given loglevel
            :type loglevel: ``int``

            """
            if logger is None:
                self.logger = log.X2GoLogger(loglevel=loglevel)
            else:
                self.logger = copy.deepcopy(logger)
            self.logger.tag = __NAME__

            self.chan = None
            self.is_active = False
            self.failed = False
            self.keepalive = None
            self.listener = listener
            self.chain_host = remote_host
            self.chain_port = remote_port
            self.ssh_transport = ssh_transport
            self.session_name = session_name
            self.session_instance = session_instance
            self.subsystem = subsystem

            self.fw_socket = None

            StreamServer.__init__(self, self.listener, self.x2go_forward_tunnel_handle)

        def start(self):
            self.keepalive = True
            return StreamServer.start(self)

        def x2go_forward_tunnel_handle(self, fw_socket, address):
            """\
            Handle for SSH/Paramiko forwarding tunnel.

            :param fw_socket: local end of the forwarding tunnel
            :type fw_socket: ``obj``
            :param address: unused/ignored
            :type address: ``tuple``

            """
            self.fw_socket = fw_socket

            # disable Nagle algorithm in TCP/IP protocol
            self.fw_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

            _success = False
            _count = 0
            _maxwait = 20

            while not _success and _count < _maxwait and self.keepalive:

                _count += 1
                try:
                    self.chan = self.ssh_transport.open_channel('direct-tcpip',
                                                                (self.chain_host, self.chain_port),
                                                                self.fw_socket.getpeername())
                    chan_peername = self.chan.getpeername()
                    _success = True
                except Exception as e:
                    if self.keepalive:
                        self.logger('incoming request to %s:%d failed on attempt %d of %d: %s' % (self.chain_host,
                                                                                                  self.chain_port,
                                                                                                  _count,
                                                                                                  _maxwait,
                                                                                                  repr(e)),
                                                                                                  loglevel=log.loglevel_WARN)
                    gevent.sleep(.4)

            if not _success:
                if self.keepalive:
                    self.logger('incoming request to %s:%d failed after %d attempts' % (self.chain_host,
                                                                                        self.chain_port,
                                                                                        _count),
                                                                                        loglevel=log.loglevel_ERROR)
                    if self.session_instance:
                        self.session_instance.set_session_name(self.session_name)
                        self.session_instance.HOOK_forwarding_tunnel_setup_failed(chain_host=self.chain_host, chain_port=self.chain_port, subsystem=self.subsystem)
                self.failed = True

            else:
                self.logger('connected!  Tunnel open %r -> %r (on master connection %r -> %r)' % (
                            self.listener, (self.chain_host, self.chain_port),
                            self.fw_socket.getpeername(), chan_peername),
                            loglevel=log.loglevel_INFO)
                # once we are here, we can presume the tunnel to be active...
                self.is_active = True

                try:
                    while self.keepalive:
                        r, w, x = select.select([self.fw_socket, self.chan], [], [])
                        if fw_socket in r:
                            data = fw_socket.recv(1024)
                            if len(data) == 0:
                                break
                            self.chan.send(data)
                        if self.chan in r:
                            data = self.chan.recv(1024)
                            if len(data) == 0:
                                break
                            fw_socket.send(data)
                    self.close_channel()
                    self.close_socket()
                except (socket.error, EOFError):
                    pass

                self.logger('Tunnel closed from %r' % (chan_peername,),
                            loglevel=log.loglevel_INFO)

        def close_channel(self):
            """\
            Close an open channel again.

            """
            #if self.chan is not None and _X2GOCLIENT_OS != "Windows":
            if self.chan is not None:
                try:
                    if _X2GOCLIENT_OS != 'Windows':
                        self.chan.close()
                    self.chan = None
                except EOFError:
                    pass

        def close_socket(self):
            """\
            Close the forwarding tunnel's socket again.

            """
            _success = False
            _count = 0
            _maxwait = 20

            # try at least <_maxwait> times
            while not _success and _count < _maxwait:
                _count += 1
                try:
                    self.close_channel()
                    if self.fw_socket is not None:
                        self.fw_socket.close()
                    _success = True
                except socket.error:
                    gevent.sleep(.2)
                    self.logger('could not close fw_tunnel socket, try again (%s of %s)' % (_count, _maxwait), loglevel=log.loglevel_WARN)

            if _count >= _maxwait:
                self.logger('forwarding tunnel to [%s]:%d could not be closed properly' % (self.chain_host, self.chain_port), loglevel=log.loglevel_WARN)

        def stop(self):
            """\
            Stop the forwarding tunnel.

            """
            self.is_active = False
            self.close_socket()
            StreamServer.stop(self)

    def start_forward_tunnel(local_host='127.0.0.1', local_port=22022,
                             remote_host='127.0.0.1', remote_port=22,
                             ssh_transport=None,
                             session_instance=None,
                             session_name=None,
                             subsystem=None,
                             logger=None, ):
        """\
        Setup up a Paramiko/SSH port forwarding tunnel (like openssh -L option).

        The tunnel is used to transport X2Go graphics data through a proxy application like nxproxy.

        :param local_host: local starting point of the forwarding tunnel (Default value = '127.0.0.1')
        :type local_host: ``int``
        :param local_port: listen port of the local starting point (Default value = 22022)
        :type local_port: ``int``
        :param remote_host: from the endpoint of the tunnel, connect to host ``<remote_host>``... (Default value = '127.0.0.1')
        :type remote_host: ``str``
        :param remote_port: on port ``<remote_port>`` (Default value = 22)
        :type remote_port: ``int``
        :param ssh_transport: the Paramiko/SSH transport (i.e. the X2Go session's Paramiko/SSH transport object) (Default value = None)
        :type ssh_transport: ``obj``
        :param session_instance: the :class:`x2go.session.X2GoSession` instance that initiates this tunnel (Default value = None)
        :type session_instance: ``obj``
        :param session_name: the session name of the X2Go session this port forwarding server belongs to (Default value = None)
        :type session_name: ``str``
        :param subsystem: a custom string with a component name that tries to evoke a new tunnel setup (Default value = None)
        :type subsystem: ``str``
        :param logger: an X2GoLogger object (Default value = None)
        :type logger: ``obj``
        :returns: returns an :class:`x2go.forward.X2GoFwServer` instance
        :rtype: ``obj``

        """
        fw_server = X2GoFwServer(listener=(local_host, local_port),
                                 remote_host=remote_host, remote_port=remote_port,
                                 ssh_transport=ssh_transport,
                                 session_instance=session_instance, session_name=session_name,
                                 subsystem=subsystem,
                                 logger=logger,
                                )
        try:
            fw_server.start()
        except socket.error:
            fw_server.failed = True
            fw_server.is_active = False

        return fw_server

    def stop_forward_tunnel(fw_server):
        """\
        Tear down a given Paramiko/SSH port forwarding tunnel.

        :param fw_server: an :class:`x2go.forward.X2GoFwServer` instance as returned by the :func:`start_forward_tunnel()` function
        :type fw_server: ``obj``

        """
        if fw_server is not None:
            fw_server.keepalive = False
            gevent.sleep(.5)
            fw_server.stop()

    if __name__ == '__main__':
        pass