ronf / asyncssh

AsyncSSH is a Python package which provides an asynchronous client and server implementation of the SSHv2 protocol on top of the Python asyncio framework.
Eclipse Public License 2.0
1.54k stars 151 forks source link

Async method call in MySFTPServer __init__ method #695

Open mertpp opened 2 days ago

mertpp commented 2 days ago

Hello I need to get the path of the chroot of the user from the database. This is how I use my sftp server to create server.

    await asyncssh.create_server(
        MySSHServer, '', PORT,
        server_host_keys=['/Users/mertpolat/.ssh/id_rsa'],
        process_factory=lambda: None,  # No shell support
        sftp_factory=lambda chan: MySFTPServer(chan, db),  
        allow_scp=True,
        kex_algs=[
            'curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256',
            'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group14-sha1'
        ],
        encryption_algs=[
            'aes128-ctr', 'aes192-ctr', 'aes256-ctr'
        ],
        mac_algs=[
            'hmac-sha2-256', 'hmac-sha2-512'
        ],
        compression_algs=['none']
    )

This is how I implemented sftp server. the problem is at the get account by username method. That method should be asynchronous. I couldn't make init method asynchronous.

Copilot suggested me to create a create factory method that returns the instance of mysftpserver. But in that case the code block above doesn't work.

class MySFTPServer(asyncssh.SFTPServer):
    def __init__(self, chan: asyncssh.SSHServerChannel, db: 'Database'):
        self._chan = chan
        self._db = db
        self._username = chan.get_extra_info('username')
        super().__init__(chan)
        username = self._username
        logger.debug(f"Fetching account data for username: {username}")
        home_folder = await self._db.get_account_by_username(username)
        print(f"Account data: {home_folder}")
        if home_folder:
            os.makedirs(home_folder, exist_ok=True)
            logger.info(f"Establishing root_folder for {username} at: {home_folder}")
            self.set_chroot(home_folder)
        else:
            logger.error(f"Account for username {username} not found in the database or data is incomplete.")
            raise ValueError(f"Account for username {username} not found in the database or data is incomplete.")   

How can I change chroot of the sftp user retrieved from the database during the initialization?

I see ssh classes has connection_made method but SFTP subclasses doesn't, so I cannot change the users chroot after initialization.

thank you for your help.

ronf commented 2 days ago

Yeah - this might be tricky at the moment, as both the server_factory and sftp_factory arguments right now only support callables, and don't allow awaitables.

One thing you might try is setting an acceptor on the call to create_server(). That can be an awaitable, and receives an SSHServerConnection as an argument. You can get the username from that connection object via get_extra_info(), similar to what the current example does using chan.get_extra_info(), do your database lookup, and then store the directory information back into the SSHServerConnection with set_extra_info(). Later, in the SFTP initialization, you'd just query for the directory via chan.get_extra_info() instead of querying for the username.

A cleaner fix would be to add the ability in AsyncSSH to use awaitables for more of the factory calls, and I've been doing that a little at a time (most recently to auth and kex exchange calls), but that has its own challenges, and can introduce race conditions if not done very carefully.

ronf commented 2 days ago

As an aside, I also noticed you are setting process_factory here, but I don't believe that should be necessary. AsyncSSH should automatically block shell/exec requests when process_factory and session_factory are not set, unless you allow it via custom callbacks in an SSHServer class. Passing in a lambda which takes no arguments might even break things, as a process factory is supposed to take an SSHServerProcess argument when it is called.