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

## x2go.sftpserver

# Source code for x2go.sftpserver

    # -*- coding: utf-8 -*-

    # Copyright (C) 2010-2023 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>

    # The Python X2Go sFTPServer code was originally written by Richard Murri,
    # for further information see his website: http://www.richardmurri.com

    # 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.

    """\
    For sharing local folders via sFTP/sshfs Python X2Go implements its own sFTP
    server (as end point of reverse forwarding tunnel requests). Thus, Python X2Go
    does not need a locally installed SSH daemon on the client side machine.

    The Python X2Go sFTP server code was originally written by Richard Murri,
    for further information see his website: http://www.richardmurri.com

    """
    __NAME__ = "x2gosftpserver-pylib"

    __package__ = 'x2go'
    __name__    = 'x2go.sftpserver'

    import os
    import shutil
    import copy
    import threading
    import paramiko
    import gevent

    # Python X2Go modules
    from . import rforward
    from . import defaults
    from . import log

    class _SSHServer(paramiko.ServerInterface):
        """\
        Implementation of a basic SSH server that is supposed
        to run with its sFTP server implementation.

        """
        def __init__(self, auth_key=None, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT, *args, **kwargs):
            """\
            Initialize a new sFTP server interface.

            :param auth_key: Server key that the client has to authenticate against
            :type auth_key: ``paramiko.RSAKey`` instance
            :param session_instance: the calling :class:`x2go.session.X2GoSession` instance
            :type session_instance: :class:`x2go.session.X2GoSession` instance
            :param logger: you can pass an :class:`x2go.log.X2GoLogger` object to the :class:`x2go.xserver.X2GoClientXConfig` 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.current_local_user = defaults.CURRENT_LOCAL_USER
            self.auth_key = auth_key
            self.session_instance = session_instance
            paramiko.ServerInterface.__init__(self, *args, **kwargs)
            logger('initializing internal SSH server for handling incoming sFTP requests, allowing connections for user ,,%s\'\' only' % self.current_local_user, loglevel=log.loglevel_DEBUG)

        def check_channel_request(self, kind, chanid):
            """\
            Only allow session requests.

            :param kind: request type
            :type kind: ``str``
            :param chanid: channel id (unused)
            :type chanid: ``any``
            :returns: returns a Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('detected a channel request for sFTP', loglevel=log.loglevel_DEBUG_SFTPXFER)
            if kind == 'session':
                return paramiko.OPEN_SUCCEEDED
            return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

        def check_auth_publickey(self, username, key):
            """\
            Ensure proper authentication.

            :param username: username of incoming authentication request
            :type username: ``str``
            :param key: incoming SSH key to be used for authentication
            :type key: ``paramiko.RSAKey`` instance
            :returns: returns a Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('sFTP server %s: username is %s' % (self, self.current_local_user), loglevel=log.loglevel_DEBUG)
            if username == self.current_local_user:
                if type(key) == paramiko.RSAKey and key == self.auth_key:
                    self.logger('sFTP server %s: publickey auth (type: %s) has been successful' % (self, key.get_name()), loglevel=log.loglevel_INFO)
                    return paramiko.AUTH_SUCCESSFUL
            self.logger('sFTP server %s: publickey (type: %s) auth failed' % (self, key.get_name()), loglevel=log.loglevel_WARN)
            return paramiko.AUTH_FAILED

        def get_allowed_auths(self, username):
            """\
            Only allow public key authentication.

            :param username: username of incoming authentication request
            :type username: ``str``
            :returns: statically returns ``publickey`` as auth mechanism
            :rtype: ``str``

            """
            self.logger('sFTP client asked for support auth methods, answering: publickey', loglevel=log.loglevel_DEBUG_SFTPXFER)
            return 'publickey'

    class _SFTPHandle(paramiko.SFTPHandle):
        """\
        Represents a handle to an open file.

        """
        def stat(self):
            """\
            Create an SFTPAttributes object from an existing stat object (an object returned by os.stat).

            :returns: new ``SFTPAttributes`` object with the same attribute fields.

            :rtype: ``obj``

            """
            try:
                return paramiko.SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)

    class _SFTPServerInterface(paramiko.SFTPServerInterface):
        """\
        sFTP server implementation.

        """
        def __init__(self, server, chroot=None, logger=None, loglevel=log.loglevel_DEFAULT, server_event=None, *args, **kwargs):
            """\
            Make user information accessible as well as set chroot jail directory.

            :param server: a ``paramiko.ServerInterface`` instance to use with this SFTP server interface
            :type server: ``paramiko.ServerInterface`` instance
            :param chroot: chroot environment for this SFTP interface
            :type chroot: ``str``
            :param logger: you can pass an :class:`x2go.log.X2GoLogger` object to the :class:`x2go.xserver.X2GoClientXConfig` 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``
            :param server_event: a ``threading.Event`` instance that can signal SFTP session termination
            :type server_event: ``threading.Event`` instance

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

            self.logger('sFTP server: initializing new channel...', loglevel=log.loglevel_DEBUG)
            self.CHROOT = chroot or '/tmp'

        def _realpath(self, path):
            """\
            Enforce the chroot jail. On Windows systems the drive letter is incorporated in the
            chroot path name (/windrive/<drive_letter>/path/to/file/or/folder).

            :param path: path name within chroot
            :type path: ``str``
            :returns: real path name (including drive letter on Windows systems)
            :rtype: ``str``

            """
            if defaults.X2GOCLIENT_OS == 'Windows' and path.startswith('/windrive'):
                _path_components = path.split('/')
                _drive = _path_components[2]
                _tail_components = (len(_path_components) > 3) and _path_components[3:] or ''
                _tail = os.path.normpath('/'.join(_tail_components))
                path = os.path.join('%s:' % _drive, '/', _tail)
            else:
                path = self.CHROOT + self.canonicalize(path)
                path = path.replace('//', '/')
            return path

        def list_folder(self, path):
            """\
            List the contents of a folder.

            :param path: path to folder
            :type path: ``str``
            :returns: returns the folder contents, on failure returns a Paramiko/SSH return code
            :rtype: ``dict`` or ``int``

            """
            path = self._realpath(path)
            self.logger('sFTP server: listing files in folder: %s' % path, loglevel=log.loglevel_DEBUG_SFTPXFER)

            try:
                out = []
                flist = os.listdir(path)
                for fname in flist:

                    try:
                        attr = paramiko.SFTPAttributes.from_stat(os.lstat(os.path.join(path, fname)))
                        attr.filename = fname
                        self.logger('sFTP server %s: file attributes ok: %s' % (self, fname), loglevel=log.loglevel_DEBUG_SFTPXFER)
                        out.append(attr)
                    except OSError as e:
                        self.logger('sFTP server %s: encountered error processing attributes of file %s: %s' % (self, fname, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)

                self.logger('sFTP server: folder list is : %s' % str([ a.filename for a in out ]), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return out
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)

        def stat(self, path):
            """\
            Stat on a file.

            :param path: path to file/folder
            :type path: ``str``
            :returns: returns the file's stat output, on failure: returns a Paramiko/SSH return code
            :rtype: ``class`` or ``int``

            """
            path = self._realpath(path)
            self.logger('sFTP server %s: calling stat on path: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            try:
                return paramiko.SFTPAttributes.from_stat(os.stat(path))
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)

        def lstat(self, path):
            """\
            LStat on a file.

            :param path: path to folder
            :type path: ``str``
            :returns: returns the file's lstat output, on failure: returns a Paramiko/SSH return code
            :rtype: ``class`` or ``int``

            """
            path = self._realpath(path)
            self.logger('sFTP server: calling lstat on path: %s' % path, loglevel=log.loglevel_DEBUG_SFTPXFER)
            try:
                return paramiko.SFTPAttributes.from_stat(os.lstat(path))
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)

        def open(self, path, flags, attr):
            """\
            Open a file for reading, writing, appending etc.

            :param path: path to file
            :type path: ``str``
            :param flags: file flags
            :type flags: ``str``
            :param attr: file attributes
            :type attr: ``class``
            :returns: file handle/object for remote file, on failure: returns a Paramiko/SSH return code
            :rtype: :class:`x2go.sftpserver._SFTPHandle` instance or ``int``

            """
            path = self._realpath(path)
            self.logger('sFTP server %s: opening file: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            try:
                binary_flag = getattr(os, 'O_BINARY',  0)
                flags |= binary_flag
                mode = getattr(attr, 'st_mode', None)
                if mode is not None:
                    fd = os.open(path, flags, mode)
                else:
                    # os.open() defaults to 0777 which is
                    # an odd default mode for files
                    fd = os.open(path, flags, 0o666)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            if (flags & os.O_CREAT) and (attr is not None):
                attr._flags &= ~attr.FLAG_PERMISSIONS
                paramiko.SFTPServer.set_file_attr(path, attr)
            if flags & os.O_WRONLY:
                if flags & os.O_APPEND:
                    fstr = 'ab'
                else:
                    fstr = 'wb'
            elif flags & os.O_RDWR:
                if flags & os.O_APPEND:
                    fstr = 'a+b'
                else:
                    fstr = 'r+b'
            else:
                # O_RDONLY (== 0)
                fstr = 'rb'
            try:
                f = os.fdopen(fd, fstr)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            fobj = _SFTPHandle(flags)
            fobj.filename = path
            fobj.readfile = f
            fobj.writefile = f
            return fobj

        def remove(self, path):
            """\
            Remove a file.

            :param path: path to file
            :type path: ``str``
            :returns: returns Paramiko/SSH return code
            :rtype: ``int``

            """
            path = self._realpath(path)
            os.remove(path)
            self.logger('sFTP server %s: removing file: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            return paramiko.SFTP_OK

        def rename(self, oldpath, newpath):
            """\
            Rename/move a file.

            :param oldpath: old path/location/file name
            :type oldpath: ``str``
            :param newpath: new path/location/file name
            :type newpath: ``str``
            :returns: returns Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('sFTP server %s: renaming path from %s to %s' % (self, oldpath, newpath), loglevel=log.loglevel_DEBUG_SFTPXFER)
            oldpath = self._realpath(oldpath)
            newpath = self._realpath(newpath)
            try:
                shutil.move(oldpath, newpath)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK

        def mkdir(self, path, attr):
            """\
            Make a directory.

            :param path: path to new folder
            :type path: ``str``
            :param attr: file attributes
            :type attr: ``class``
            :returns: returns Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('sFTP server: creating new dir (perms: %s): %s' % (attr.st_mode, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            path = self._realpath(path)
            try:
                os.mkdir(path, attr.st_mode)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK

        def rmdir(self, path):
            """\
            Remove a directory (if needed recursively).

            :param path: folder to be removed
            :type path: ``str``
            :returns: returns Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('sFTP server %s: removing dir: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            path = self._realpath(path)
            try:
                shutil.rmtree(path)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK

        def chattr(self, path, attr):
            """\
            Change file attributes.

            :param path: path of file/folder
            :type path: ``str``
            :param attr: new file attributes
            :type attr: ``class``
            :returns: returns Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('sFTP server %s: modifying attributes of path: %s' % (self, path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            path = self._realpath(path)
            try:
                if attr.st_mode is not None:
                    os.chmod(path, attr.st_mode)
                if attr.st_uid is not None:
                    os.chown(path, attr.st_uid, attr.st_gid)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK

        def symlink(self, target_path, path):
            """\
            Create a symbolic link.

            :param target_path: link shall point to this path
            :type target_path: ``str``
            :param path: link location
            :type path: ``str``
            :returns: returns Paramiko/SSH return code
            :rtype: ``int``

            """
            self.logger('sFTP server %s: creating symlink from: %s to target: %s' % (self, path, target_path), loglevel=log.loglevel_DEBUG_SFTPXFER)
            path = self._realpath(path)
            if target_path.startswith('/'):
                target_path = self._realpath(target_path)
            try:
                os.symlink(target_path, path)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK

        def readlink(self, path):
            """\
            Read the target of a symbolic link.

            :param path: path of symbolic link
            :type path: ``str``
            :returns: target location of the symbolic link, on failure: returns a Paramiko/SSH return code
            :rtype: ``str`` or ``int``

            """
            path = self._realpath(path)
            try:
                return os.readlink(path)
            except OSError as e:
                self.logger('sFTP server %s: encountered error: %s' % (self, str(e)), loglevel=log.loglevel_DEBUG_SFTPXFER)
                return paramiko.SFTPServer.convert_errno(e.errno)

        def session_ended(self):
            """\
            Tidy up when the sFTP session has ended.

            """
            if self.server_event is not None:
                self.logger('sFTP server %s: session has ended' % self, loglevel=log.loglevel_DEBUG_SFTPXFER)
                self.server_event.set()

    class X2GoRevFwTunnelToSFTP(rforward.X2GoRevFwTunnel):
        """\
        A reverse fowarding tunnel with an sFTP server at its endpoint. This blend of a Paramiko/SSH
        reverse forwarding tunnel is used to provide access to local X2Go client folders
        from within the the remote X2Go server session.

        """
        def __init__(self, server_port, ssh_transport, auth_key=None, session_instance=None, logger=None, loglevel=log.loglevel_DEFAULT):
            """\
            Start a Paramiko/SSH reverse forwarding tunnel, that has an sFTP server listening at
            the endpoint of the tunnel.

            :param server_port: the TCP/IP port on the X2Go server (starting point of the tunnel),
                normally some number above 30000
            :type server_port: ``int``
            :param ssh_transport: the :class:`x2go.session.X2GoSession`'s Paramiko/SSH transport instance
            :type ssh_transport: ``paramiko.Transport`` instance
            :param auth_key: Paramiko/SSH RSAkey object that has to be authenticated against by
                the remote sFTP client
            :type auth_key: ``paramiko.RSAKey`` instance
            :param logger: you can pass an :class:`x2go.log.X2GoLogger` object to the
                :class:`x2go.sftpserver.X2GoRevFwTunnelToSFTP` constructor
            :type logger: :class:`x2go.log.X2GoLogger` instance
            :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``

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

            self.server_port = server_port
            self.ssh_transport = ssh_transport
            self.session_instance = session_instance
            if type(auth_key) is not paramiko.RSAKey:
                auth_key = None
            self.auth_key = auth_key

            self.open_channels = {}
            self.incoming_channel = threading.Condition()

            threading.Thread.__init__(self)
            self.daemon = True
            self._accept_channels = True

        def run(self):
            """\
            This method gets run once an :class:`x2go.sftpserver.X2GoRevFwTunnelToSFTP` has been started with its
            :func:`start()` method. Use :func:`X2GoRevFwTunnelToSFTP.stop_thread() <x2go.sftpserver.X2GoRevFwTunnelToSFTP.stop_thread()>` to stop the
            reverse forwarding tunnel again (refer also to its pause() and resume() method).

            :func:`X2GoRevFwTunnelToSFTP.run() <x2go.rforward.X2GoRevFwTunnelToSFTP.run()>` waits for notifications of an appropriate incoming
            Paramiko/SSH channel (issued by :func:`X2GoRevFwTunnelToSFTP.notify() <x2go.rforward.X2GoRevFwTunnelToSFTP.notify()>`). Appropriate in
            this context means, that its starting point on the X2Go server matches the class's
            property ``server_port``.

            Once a new incoming channel gets announced by the :func:`notify()` method, a new
            :class:`x2go.sftpserver.X2GoRevFwSFTPChannelThread` instance will be initialized. As a data stream handler,
            the function :func:`x2go_rev_forward_sftpchannel_handler()` will be used.

            The channel will last till the connection gets dropped on the X2Go server side or
            until the tunnel gets paused by an :func:`X2GoRevFwTunnelToSFTP.pause() <x2go.rforward.X2GoRevFwTunnelToSFTP.pause()>` call or
            stopped via the ``X2GoRevFwTunnelToSFTP.stop_thread()`` method.

            """
            self._request_port_forwarding()
            self._keepalive = True
            self.ready = True
            while self._keepalive:

                self.incoming_channel.acquire()

                self.logger('waiting for incoming sFTP channel on X2Go server port: [localhost]:%s' % self.server_port, loglevel=log.loglevel_DEBUG)
                self.incoming_channel.wait()
                if self._keepalive:
                    self.logger('Detected incoming sFTP channel on X2Go server port: [localhost]:%s' % self.server_port, loglevel=log.loglevel_DEBUG)
                    _chan = self.ssh_transport.accept()
                    self.logger('sFTP channel %s for server port [localhost]:%s is up' % (_chan, self.server_port), loglevel=log.loglevel_DEBUG)
                else:
                    self.logger('closing down rev forwarding sFTP tunnel on remote end [localhost]:%s' % self.server_port, loglevel=log.loglevel_DEBUG)

                self.incoming_channel.release()
                if self._accept_channels and self._keepalive:
                    _new_chan_thread = X2GoRevFwSFTPChannelThread(_chan,
                                                                  target=x2go_rev_forward_sftpchannel_handler,
                                                                  kwargs={
                                                                   'chan': _chan,
                                                                   'auth_key': self.auth_key,
                                                                   'logger': self.logger,
                                                                  }
                                                                 )
                    _new_chan_thread.start()
                    self.open_channels['[%s]:%s' % _chan.origin_addr] = _new_chan_thread
            self.ready = False

    def x2go_rev_forward_sftpchannel_handler(chan=None, auth_key=None, logger=None):
        """\
        Handle incoming sFTP channels that got setup by an :class:`x2go.sftpserver.X2GoRevFwTunnelToSFTP` instance.

        The channel (and the corresponding connections) close either ...

            - ... if the connecting application closes the connection and thus, drops
              the sFTP channel, or
            - ... if the :class:`x2go.sftpserver.X2GoRevFwTunnelToSFTP` parent thread gets paused. The call
              of :func:`X2GoRevFwTunnelToSFTP.pause() <x2go.rforward.X2GoRevFwTunnelToSFTP.pause()>` on the instance can be used to shut down all incoming
              tunneled SSH connections associated to this :class:`x2go.sftpserver.X2GoRevFwTunnelToSFTP` instance
              from within a Python X2Go application.

        :param chan: an incoming sFTP channel (Default value = None)
        :type chan: paramiko.Channel instance
        :param auth_key: Paramiko/SSH RSAkey object that has to be authenticated against by
            the remote sFTP client (Default value = None)
        :type auth_key: ``paramiko.RSAKey`` instance
        :param logger: you must pass an :class:`x2go.log.X2GoLogger` object to this handler method (Default value = None)
        :type logger: ``X2GoLogger`` instance

        """
        if logger is None:
            def _dummy_logger(msg, l):
                pass
            logger = _dummy_logger

        if auth_key is None:
            logger('sFTP channel %s closed because of missing authentication key' % chan, loglevel=log.loglevel_DEBUG)
            return

        # set up server
        t = paramiko.Transport(chan)
        t.daemon = True
        t.load_server_moduli()
        t.add_server_key(defaults.RSAHostKey)

        # set up sftp handler, server and event
        event = threading.Event()
        t.set_subsystem_handler('sftp', paramiko.SFTPServer, sftp_si=_SFTPServerInterface, chroot='/', logger=logger, server_event=event)
        logger('registered sFTP subsystem handler', loglevel=log.loglevel_DEBUG_SFTPXFER)
        server = _SSHServer(auth_key=auth_key, logger=logger)

        # start ssh server session
        t.start_server(server=server, event=event)

        while t.is_active():
            gevent.sleep(1)

        t.stop_thread()
        logger('sFTP channel %s closed down' % chan, loglevel=log.loglevel_DEBUG)

    class X2GoRevFwSFTPChannelThread(rforward.X2GoRevFwChannelThread): pass

    """A clone of :class:`x2go.rforward.X2GoRevFwChannelThread`."""