"""
fs.expose.sftp
==============

Expose an FS object over SFTP (via paramiko).

This module provides the necessary interfaces to expose an FS object over
SFTP, plugging into the infrastructure provided by the 'paramiko' module.

For simple usage, the class 'BaseSFTPServer' provides an all-in-one server
class based on the standard SocketServer module.  Use it like so::

    server = BaseSFTPServer((hostname,port),fs)
    server.serve_forever()

Note that the base class allows UNAUTHENTICATED ACCESS by default.  For more
serious work you will probably want to subclass it and override methods such
as check_auth_password() and get_allowed_auths().

To integrate this module into an existing server framework based on paramiko,
the 'SFTPServerInterface' class provides a concrete implementation of the
paramiko.SFTPServerInterface protocol.  If you don't understand what this
is, you probably don't want to use it.

"""



import os
import stat as statinfo
import time
import socketserver
import threading

import paramiko

from fs.base import flags_to_mode
from fs.path import *
from fs.errors import *
from fs.local_functools import wraps
from fs.filelike import StringIO
from fs.utils import isdir


# Default host key used by BaseSFTPServer
#
DEFAULT_HOST_KEY = paramiko.RSAKey.from_private_key(StringIO(
    "-----BEGIN RSA PRIVATE KEY-----\n" \
    "MIICXgIBAAKCAIEAl7sAF0x2O/HwLhG68b1uG8KHSOTqe3Cdlj5i/1RhO7E2BJ4B\n" \
    "3jhKYDYtupRnMFbpu7fb21A24w3Y3W5gXzywBxR6dP2HgiSDVecoDg2uSYPjnlDk\n" \
    "HrRuviSBG3XpJ/awn1DObxRIvJP4/sCqcMY8Ro/3qfmid5WmMpdCZ3EBeC0CAwEA\n" \
    "AQKCAIBSGefUs5UOnr190C49/GiGMN6PPP78SFWdJKjgzEHI0P0PxofwPLlSEj7w\n" \
    "RLkJWR4kazpWE7N/bNC6EK2pGueMN9Ag2GxdIRC5r1y8pdYbAkuFFwq9Tqa6j5B0\n" \
    "GkkwEhrcFNBGx8UfzHESXe/uE16F+e8l6xBMcXLMJVo9Xjui6QJBAL9MsJEx93iO\n" \
    "zwjoRpSNzWyZFhiHbcGJ0NahWzc3wASRU6L9M3JZ1VkabRuWwKNuEzEHNK8cLbRl\n" \
    "TyH0mceWXcsCQQDLDEuWcOeoDteEpNhVJFkXJJfwZ4Rlxu42MDsQQ/paJCjt2ONU\n" \
    "WBn/P6iYDTvxrt/8+CtLfYc+QQkrTnKn3cLnAkEAk3ixXR0h46Rj4j/9uSOfyyow\n" \
    "qHQunlZ50hvNz8GAm4TU7v82m96449nFZtFObC69SLx/VsboTPsUh96idgRrBQJA\n" \
    "QBfGeFt1VGAy+YTLYLzTfnGnoFQcv7+2i9ZXnn/Gs9N8M+/lekdBFYgzoKN0y4pG\n" \
    "2+Q+Tlr2aNlAmrHtkT13+wJAJVgZATPI5X3UO0Wdf24f/w9+OY+QxKGl86tTQXzE\n" \
    "4bwvYtUGufMIHiNeWP66i6fYCucXCMYtx6Xgu2hpdZZpFw==\n" \
    "-----END RSA PRIVATE KEY-----\n"
))


def report_sftp_errors(func):
    """Decorator to catch and report FS errors as SFTP error codes.

    Any FSError exceptions are caught and translated into an appropriate
    return code, while other exceptions are passed through untouched.
    """
    @wraps(func)
    def wrapper(*args,**kwds):
        try:
            return func(*args, **kwds)
        except ResourceNotFoundError as e:
            return paramiko.SFTP_NO_SUCH_FILE
        except UnsupportedError as e:
            return paramiko.SFTP_OP_UNSUPPORTED
        except FSError as e:
            return paramiko.SFTP_FAILURE
    return wrapper


class SFTPServerInterface(paramiko.SFTPServerInterface):
    """SFTPServerInterface implementation that exposes an FS object.

    This SFTPServerInterface subclass expects a single additional argument,
    the fs object to be exposed.  Use it to set up a transport subsystem
    handler like so::

      t.set_subsystem_handler("sftp",SFTPServer,SFTPServerInterface,fs)

    If this all looks too complicated, you might consider the BaseSFTPServer
    class also provided by this module - it automatically creates the enclosing
    paramiko server infrastructure.
    """

    def __init__(self, server, fs, encoding=None, *args, **kwds):
        self.fs = fs
        if encoding is None:
            encoding = "utf8"
        self.encoding = encoding
        super(SFTPServerInterface,self).__init__(server, *args, **kwds)

    def close(self):
        # Close the pyfs file system and dereference it.
        self.fs.close()
        self.fs = None

    @report_sftp_errors
    def open(self, path, flags, attr):
        return SFTPHandle(self, path, flags)

    @report_sftp_errors
    def list_folder(self, path):
        if not isinstance(path, str):
            path = path.decode(self.encoding)
        stats = []
        for entry in self.fs.listdir(path, absolute=True):
            stat = self.stat(entry)
            if not isinstance(stat, int):
                stats.append(stat)
        return stats

    @report_sftp_errors
    def stat(self, path):
        if not isinstance(path, str):
            path = path.decode(self.encoding)

        info = self.fs.getinfo(path)

        stat = paramiko.SFTPAttributes()
        stat.filename = basename(path).encode(self.encoding)
        stat.st_size = info.get("size")

        if 'st_atime' in info:
            stat.st_atime = info.get('st_atime')
        elif 'accessed_time' in info:
            stat.st_atime = time.mktime(info.get("accessed_time").timetuple())

        if 'st_mtime' in info:
            stat.st_mtime = info.get('st_mtime')
        else:
            if 'modified_time' in info:
                stat.st_mtime = time.mktime(info.get("modified_time").timetuple())

        if isdir(self.fs, path, info):
            stat.st_mode = 0o777 | statinfo.S_IFDIR
        else:
            stat.st_mode = 0o777 | statinfo.S_IFREG
        return stat

    def lstat(self, path):
        return self.stat(path)

    @report_sftp_errors
    def remove(self, path):
        if not isinstance(path, str):
            path = path.decode(self.encoding)
        self.fs.remove(path)
        return paramiko.SFTP_OK

    @report_sftp_errors
    def rename(self, oldpath, newpath):
        if not isinstance(oldpath, str):
            oldpath = oldpath.decode(self.encoding)
        if not isinstance(newpath, str):
            newpath = newpath.decode(self.encoding)
        if self.fs.isfile(oldpath):
            self.fs.move(oldpath, newpath)
        else:
            self.fs.movedir(oldpath, newpath)
        return paramiko.SFTP_OK

    @report_sftp_errors
    def mkdir(self, path, attr):
        if not isinstance(path, str):
            path = path.decode(self.encoding)
        self.fs.makedir(path)
        return paramiko.SFTP_OK

    @report_sftp_errors
    def rmdir(self, path):
        if not isinstance(path, str):
            path = path.decode(self.encoding)
        self.fs.removedir(path)
        return paramiko.SFTP_OK

    def canonicalize(self, path):
        try:
            return abspath(normpath(path)).encode(self.encoding)
        except BackReferenceError:
            # If the client tries to use backrefs to escape root, gently
            # nudge them back to /.
            return '/'

    @report_sftp_errors
    def chattr(self, path, attr):
        #  f.truncate() is implemented by setting the size attr.
        #  Any other attr requests fail out.
        if attr._flags:
            if attr._flags != attr.FLAG_SIZE:
                raise UnsupportedError
            with self.fs.open(path,"r+") as f:
                f.truncate(attr.st_size)
        return paramiko.SFTP_OK

    def readlink(self, path):
        return paramiko.SFTP_OP_UNSUPPORTED

    def symlink(self, path):
        return paramiko.SFTP_OP_UNSUPPORTED


class SFTPHandle(paramiko.SFTPHandle):
    """SFTP file handler pointing to a file in an FS object.

    This is a simple file wrapper for SFTPServerInterface, passing read
    and write requests directly through the to underlying file from the FS.
    """

    def __init__(self, owner, path, flags):
        super(SFTPHandle, self).__init__(flags)
        mode = flags_to_mode(flags)
        self.owner = owner
        if not isinstance(path, str):
            path = path.decode(self.owner.encoding)
        self.path = path
        self._file = owner.fs.open(path, mode)

    @report_sftp_errors
    def close(self):
        self._file.close()
        return paramiko.SFTP_OK

    @report_sftp_errors
    def read(self, offset, length):
        self._file.seek(offset)
        return self._file.read(length)

    @report_sftp_errors
    def write(self, offset, data):
        self._file.seek(offset)
        self._file.write(data)
        return paramiko.SFTP_OK

    def stat(self):
        return self.owner.stat(self.path)

    def chattr(self,attr):
        return self.owner.chattr(self.path, attr)


class SFTPServer(paramiko.SFTPServer):
    """
    An SFTPServer class that closes the filesystem when done.
    """

    def finish_subsystem(self):
        # Close the SFTPServerInterface, it will close the pyfs file system.
        self.server.close()
        super(SFTPServer, self).finish_subsystem()


class SFTPRequestHandler(socketserver.BaseRequestHandler):
    """SocketServer RequestHandler subclass for BaseSFTPServer.

    This RequestHandler subclass creates a paramiko Transport, sets up the
    sftp subsystem, and hands off to the transport's own request handling
    thread.
    """
    timeout = 60
    auth_timeout = 60

    def setup(self):
        """
        Creates the SSH transport. Sets security options.
        """
        self.transport = paramiko.Transport(self.request)
        self.transport.load_server_moduli()
        so = self.transport.get_security_options()
        so.digests = ('hmac-sha1', )
        so.compression = ('zlib@openssh.com', 'none')
        self.transport.add_server_key(self.server.host_key)
        self.transport.set_subsystem_handler("sftp", SFTPServer, SFTPServerInterface, self.server.fs, encoding=self.server.encoding)

    def handle(self):
        """
        Start the paramiko server, this will start a thread to handle the connection.
        """
        self.transport.start_server(server=BaseServerInterface())
        # TODO: I like the code below _in theory_ but it does not work as I expected.
        # Figure out how to actually time out a new client if they fail to auth in a
        # certain amount of time.
        #chan = self.transport.accept(self.auth_timeout)
        #if chan is None:
        #    self.transport.close()

    def handle_timeout(self):
        try:
            self.transport.close()
        finally:
            super(SFTPRequestHandler, self).handle_timeout()



class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass


class BaseSFTPServer(ThreadedTCPServer):
    """SocketServer.TCPServer subclass exposing an FS via SFTP.

    Operation is in the standard SocketServer style.  The target FS object
    can be passed into the constructor, or set as an attribute on the server::

        server = BaseSFTPServer((hostname,port),fs)
        server.serve_forever()

    It is also possible to specify the host key used by the sever by setting
    the 'host_key' attribute.  If this is not specified, it will default to
    the key found in the DEFAULT_HOST_KEY variable.
    """
    # If the server stops/starts quickly, don't fail because of
    # "port in use" error.
    allow_reuse_address = True

    def __init__(self, address, fs=None, encoding=None, host_key=None, RequestHandlerClass=None):
        self.fs = fs
        self.encoding = encoding
        if host_key is None:
            host_key = DEFAULT_HOST_KEY
        self.host_key = host_key
        if RequestHandlerClass is None:
            RequestHandlerClass = SFTPRequestHandler
        socketserver.TCPServer.__init__(self, address, RequestHandlerClass)

    def shutdown_request(self, request):
        # Prevent TCPServer from closing the connection prematurely
        return

    def close_request(self, request):
        # Prevent TCPServer from closing the connection prematurely
        return


class BaseServerInterface(paramiko.ServerInterface):
    """
    Paramiko ServerInterface implementation that performs user authentication.

    Note that this base class allows UNAUTHENTICATED ACCESS to the exposed
    FS.  This is intentional, since we can't guess what your authentication
    needs are.  To protect the exposed FS, override the following methods:

        * get_allowed_auths Determine the allowed auth modes
        * check_auth_none Check auth with no credentials
        * check_auth_password Check auth with a password
        * check_auth_publickey Check auth with a public key
    """

    def check_channel_request(self, kind, chanid):
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_auth_none(self, username):
        """Check whether the user can proceed without authentication."""
        return paramiko.AUTH_SUCCESSFUL

    def check_auth_publickey(self, username,key):
        """Check whether the given public key is valid for authentication."""
        return paramiko.AUTH_FAILED

    def check_auth_password(self, username, password):
        """Check whether the given password is valid for authentication."""
        return paramiko.AUTH_FAILED

    def get_allowed_auths(self,username):
        """Return string containing a comma separated list of allowed auth modes.

        The available modes are  "node", "password" and "publickey".
        """
        return "none"


#  When called from the command-line, expose a TempFS for testing purposes
if __name__ == "__main__":
    from fs.tempfs import TempFS
    server = BaseSFTPServer(("localhost",8022),TempFS())
    try:
        #import rpdb2; rpdb2.start_embedded_debugger('password')
        server.serve_forever()
    except (SystemExit,KeyboardInterrupt):
        server.server_close()

