pythonserversftpparamiko

SFTP server in Python with Paramiko does not allow SFTP connections


I rewrote the question because it didn't fit underneath.

I'm trying paramiko library, in python, to configure an SFTP Server.

I start defining a ROOT_FOLDER to the path of my wished one.

Then i exend paramiko.SFTPServerInteface:

class SFTPServer(paramiko.SFTPServerInterface):
    def __init__(self, server: paramiko.ServerInterface, *args, **kwargs):
        super().__init__(server)
        self.root_folder = ROOT_FOLDER
    
    def _resolve_path(self, path):
        safe_path = os.path.normpath(os.path.join(self.root_folder, path.lstrip('\\')))
        if not safe_path.startswith(self.root_folder):
            raise IOError("Access denied.")  # Blocca accesso esterno a ROOT_FOLDER
        return safe_path

    def list_folder(self, path):
        logging.debug("Listing folder files.")
        full_path = self._resolve_path(path)
        files = os.listdir(full_path)
        return [paramiko.SFTPAttributes.from_stat(os.stat(os.path.join(full_path, f))) for f in files]

    def stat(self, path):
        full_path = self._resolve_path(path)
        return paramiko.SFTPAttributes.from_stat(os.stat(full_path))

    def open(self, path, flags, attr):
        full_path = self._resolve_path(path)
        mode = 'rb' if flags & os.O_RDONLY else 'wb'
        return open(full_path, mode)

    def remove(self, path):
        full_path = self._resolve_path(path)
        os.remove(full_path)

And then the paramiko.ServerInterface:

class SSHServer(paramiko.ServerInterface):
    def __init__(self):
        super().__init__()
        self.event = threading.Event()

    def check_auth_password(self, username, password):
        if authenticate_user(username, password=password):
            logging.debug(f"User {username} authenticated by password.")
            return paramiko.AUTH_SUCCESSFUL
        else:
            logging.debug(f"Failed password authentication for user {username}.")
            return paramiko.AUTH_FAILED

    def check_auth_publickey(self, username, key):
        public_key = key.get_base64()
        if authenticate_user(username, public_key=public_key):
            logging.debug(f"User {username} authenticated by public key.")
            return paramiko.AUTH_SUCCESSFUL
        else:
            logging.debug(f"Failed public key authentication for user {username}.")
            return paramiko.AUTH_FAILED

    def check_channel_request(self, kind, chanid):
        logging.debug(f"Channel request type: {kind}")
        if kind == 'session':
            return paramiko.OPEN_SUCCEEDED
        else:
            logging.debug(f"Channel type not supported: {kind}")
            return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

    def check_channel_subsystem_request(self, channel, name):
        logging.debug(f"Channel request type (subsystem): {name}")
        if name == 'sftp':
            paramiko.SFTPServer(channel, 'sftp', self, SFTPServer)
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
    
    def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes):
        return True

    def check_channel_shell_request(self, channel):
        self.event.set()
        return True

    def check_channel_exec_request(self, channel, command):
        channel.send(b"Esecuzione del comando: " + command)
        channel.send_exit_status(0)
        return True

I don't know if shell, pty and exec requsts are correctly handled.

In this function i handle client connection, but i don't know if i had to write "transport.set_subsystem_handler" or the commented lines:

def handle_client(client_socket: socket.socket, host_key: paramiko.RSAKey):

    try:
        
        transport = paramiko.Transport(client_socket)
        transport.add_server_key(host_key)

        ssh_server = SSHServer()
        transport.set_subsystem_handler('sftp', paramiko.SFTPServer, SFTPServer)

        try:
            transport.start_server(server=ssh_server)
        except paramiko.SSHException as e:
            logging.error(f"SSH negotiation failed: {e}")
            return

        channel = transport.accept(40)
        if channel is None:
            logging.error("No channel found")
            return

        logging.info("Client authenticated.")
        logging.debug("Starting SFTP session.")
        
        #sftp_server = paramiko.SFTPServer(transport, sftp_si=SFTPServer(ssh_server, ROOT_FOLDER))
        #sftp_server.start()

        while not channel.closed:
            time.sleep(1)
        
    except paramiko.SSHException as e:
        logging.error(f"SSH exception: {e}")
    except Exception as e:
        logging.error(f"Error handling client: {e}")
    finally:
        logging.debug("Closing channel.")
        if channel != None:
            channel.close()

Then to start my server I use this function:

def start_sftp_server():
    logging.info("Starting SFTP server...")
    
    host_key = paramiko.RSAKey.generate(2048)
    
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.bind(('localhost', 2222))
    server_socket.listen(100)
    
    client_socket, addr = server_socket.accept()
    
    transport = paramiko.Transport(client_socket)
    transport.add_server_key(host_key)
    
    server = SSHServer()
    
    transport.start_server(server=server)
    
    channel = transport.accept()
    logging.info("SFTP Server started.")
    #transport.set_subsystem_handler('sftp', paramiko.SFTPServer(channel, 'sftp', server, SFTPServer))
    while transport.is_active():
        pass

When i try to connect to the server using softwares like WinSCP or FileZilla, the session starts but it remains stucked here, i can't see files and client timeout expires.

Here paramiko logs:

DEB [20241130-20:28:59.272] thr=1   paramiko.transport: starting thread (server mode): 0x7e1c8ec0
DEB [20241130-20:28:59.273] thr=1   paramiko.transport: Local version/idstring: SSH-2.0-paramiko_3.5.0
DEB [20241130-20:28:59.338] thr=1   paramiko.transport: Remote version/idstring: SSH-2.0-WinSCP_release_6.3.6
INF [20241130-20:28:59.339] thr=1   paramiko.transport: Connected (version 2.0, client WinSCP_release_6.3.6)
DEB [20241130-20:28:59.341] thr=1   paramiko.transport: === Key exchange possibilities ===
DEB [20241130-20:28:59.341] thr=1   paramiko.transport: kex algos: sntrup761x25519-sha512@openssh.com, curve448-sha512, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group18-sha512, diffie-hellman-group17-sha512, diffie-hellman-group16-sha512, diffie-hellman-group15-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1, rsa2048-sha256, rsa1024-sha1, diffie-hellman-group1-sha1, ext-info-c, kex-strict-c-v00@openssh.com
DEB [20241130-20:28:59.342] thr=1   paramiko.transport: server key: rsa-sha2-512, rsa-sha2-256, ssh-rsa, ssh-ed448, ssh-ed25519, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, ecdsa-sha2-nistp521, ssh-dss
DEB [20241130-20:28:59.343] thr=1   paramiko.transport: client encrypt: aes256-ctr, aes256-cbc, rijndael-cbc@lysator.liu.se, aes192-ctr, aes192-cbc, aes128-ctr, aes128-cbc, chacha20-poly1305@openssh.com, aes128-gcm@openssh.com, aes256-gcm@openssh.com, 3des-ctr, 3des-cbc, blowfish-ctr, blowfish-cbc, arcfour256, arcfour128
DEB [20241130-20:28:59.343] thr=1   paramiko.transport: server encrypt: aes256-ctr, aes256-cbc, rijndael-cbc@lysator.liu.se, aes192-ctr, aes192-cbc, aes128-ctr, aes128-cbc, chacha20-poly1305@openssh.com, aes128-gcm@openssh.com, aes256-gcm@openssh.com, 3des-ctr, 3des-cbc, blowfish-ctr, blowfish-cbc, arcfour256, arcfour128
DEB [20241130-20:28:59.344] thr=1   paramiko.transport: client mac: hmac-sha2-256, hmac-sha2-512, hmac-sha1, hmac-sha1-96, hmac-md5, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, hmac-sha1-96-etm@openssh.com, hmac-md5-etm@openssh.com
DEB [20241130-20:28:59.344] thr=1   paramiko.transport: server mac: hmac-sha2-256, hmac-sha2-512, hmac-sha1, hmac-sha1-96, hmac-md5, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, hmac-sha1-96-etm@openssh.com, hmac-md5-etm@openssh.com
DEB [20241130-20:28:59.344] thr=1   paramiko.transport: client compress: none, zlib, zlib@openssh.com
DEB [20241130-20:28:59.344] thr=1   paramiko.transport: server compress: none, zlib, zlib@openssh.com
DEB [20241130-20:28:59.345] thr=1   paramiko.transport: client lang: <none>
DEB [20241130-20:28:59.345] thr=1   paramiko.transport: server lang: <none>
DEB [20241130-20:28:59.346] thr=1   paramiko.transport: kex follows: False
DEB [20241130-20:28:59.346] thr=1   paramiko.transport: === Key exchange agreements ===
DEB [20241130-20:28:59.346] thr=1   paramiko.transport: Strict kex mode: True
DEB [20241130-20:28:59.347] thr=1   paramiko.transport: Kex: curve25519-sha256@libssh.org
DEB [20241130-20:28:59.347] thr=1   paramiko.transport: HostKey: rsa-sha2-512
DEB [20241130-20:28:59.347] thr=1   paramiko.transport: Cipher: aes256-ctr
DEB [20241130-20:28:59.348] thr=1   paramiko.transport: MAC: hmac-sha2-256
DEB [20241130-20:28:59.348] thr=1   paramiko.transport: Compression: none
DEB [20241130-20:28:59.348] thr=1   paramiko.transport: === End of kex handshake ===
DEB [20241130-20:28:59.357] thr=1   paramiko.transport: Resetting outbound seqno after NEWKEYS due to strict mode
DEB [20241130-20:28:59.357] thr=1   paramiko.transport: kex engine KexCurve25519 specified hash_algo <built-in function openssl_sha256>
DEB [20241130-20:29:00.480] thr=1   paramiko.transport: Switch to new keys ...
DEB [20241130-20:29:00.480] thr=1   paramiko.transport: Resetting inbound seqno after NEWKEYS due to strict mode
DEB [20241130-20:29:00.512] thr=1   paramiko.transport: Auth request (type=none) service=ssh-connection, username=MagoLione
INF [20241130-20:29:00.512] thr=1   paramiko.transport: Auth rejected (none).
DEB [20241130-20:29:00.544] thr=1   paramiko.transport: Auth request (type=password) service=ssh-connection, username=MagoLione
INF [20241130-20:29:01.030] thr=1   paramiko.transport: Auth granted (password).
DEB [20241130-20:29:01.031] thr=1   paramiko.transport: [chan 0] Max packet in: 32768 bytes
DEB [20241130-20:29:01.031] thr=1   paramiko.transport: [chan 0] Max packet out: 16384 bytes
DEB [20241130-20:29:01.031] thr=1   paramiko.transport: Secsh channel 0 (session) opened.
DEB [20241130-20:29:01.032] thr=1   paramiko.transport: [chan 0] Unhandled channel request "simple@putty.projects.tartarus.org"
DEB [20241130-20:29:20.501] thr=1   paramiko.transport: [chan 0] EOF received (0)
DEB [20241130-20:29:21.033] thr=1   paramiko.transport: EOF in transport thread

Here are WinSCP logs:

. 2024-11-30 20:28:58.167 --------------------------------------------------------------------------
. 2024-11-30 20:28:58.167 WinSCP Versione 6.3.6 (Build 15073 2024-11-25) (OS 10.0.22621 – Windows 10 Home)
. 2024-11-30 20:28:58.167 Configuration: HKCU\Software\Martin Prikryl\WinSCP 2\
. 2024-11-30 20:28:58.167 Log level: Debug 2
. 2024-11-30 20:28:58.167 Local account: PC-MAGOLIONE\simon
. 2024-11-30 20:28:58.167 Working directory: C:\Program Files (x86)\WinSCP
. 2024-11-30 20:28:58.167 Process ID: 13376
. 2024-11-30 20:28:58.181 Ancestor processes: ...
. 2024-11-30 20:28:58.182 Command-line: "C:\Program Files (x86)\WinSCP\WinSCP.exe"
. 2024-11-30 20:28:58.182 Time zone: Current: GMT+1, Standard: GMT+1 (ora solare Europa occidentale), DST: GMT+2 (ora legale Europa occidentale), DST Start: 31/03/2024, DST End: 27/10/2024
. 2024-11-30 20:28:58.183 Login time: sabato 30 novembre 2024 20:28:58
. 2024-11-30 20:28:58.183 --------------------------------------------------------------------------
. 2024-11-30 20:28:58.183 Session name: MagoLione@localhost (Site)
. 2024-11-30 20:28:58.183 Host name: localhost (Port: 2222)
. 2024-11-30 20:28:58.183 User name: MagoLione (Password: Yes, Key file: No, Passphrase: No)
. 2024-11-30 20:28:58.183 Tunnel: No
. 2024-11-30 20:28:58.183 Transfer Protocol: SFTP (SCP)
. 2024-11-30 20:28:58.183 Ping type: Off, Ping interval: 30 sec; Timeout: 15 sec
. 2024-11-30 20:28:58.183 Disable Nagle: No
. 2024-11-30 20:28:58.183 Proxy: None
. 2024-11-30 20:28:58.183 Send buffer: 262144
. 2024-11-30 20:28:58.183 Compression: No
. 2024-11-30 20:28:58.183 Bypass authentication: No
. 2024-11-30 20:28:58.183 Try agent: Yes; Agent forwarding: No; KI: Yes; GSSAPI: Yes
. 2024-11-30 20:28:58.183 GSSAPI: KEX: No; Forwarding: No; Libs: gssapi32,sspi,custom; Custom: 
. 2024-11-30 20:28:58.183 Ciphers: aes,chacha20,aesgcm,3des,WARN,des,blowfish,arcfour; Ssh2DES: No
. 2024-11-30 20:28:58.183 KEX: ntru-curve25519,ecdh,dh-gex-sha1,dh-group18-sha512,dh-group17-sha512,dh-group16-sha512,dh-group15-sha512,dh-group14-sha1,rsa,WARN,dh-group1-sha1
. 2024-11-30 20:28:58.183 SSH Bugs: Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto
. 2024-11-30 20:28:58.183 Simple channel: Yes
. 2024-11-30 20:28:58.183 Return code variable: Autodetect; Lookup user groups: Auto
. 2024-11-30 20:28:58.183 Shell: default
. 2024-11-30 20:28:58.183 EOL: LF, UTF: Auto
. 2024-11-30 20:28:58.183 Clear aliases: Yes, Unset nat.vars: Yes, Resolve symlinks: Yes; Follow directory symlinks: No
. 2024-11-30 20:28:58.183 LS: ls -la, Ign LS warn: Yes, Scp1 Comp: No; Exit code 1 is error: No
. 2024-11-30 20:28:58.184 SFTP Bugs: Auto,Auto
. 2024-11-30 20:28:58.184 SFTP Server: default
. 2024-11-30 20:28:58.184 Local directory: default, Remote directory: home, Update: Yes, Cache: Yes
. 2024-11-30 20:28:58.184 Cache directory changes: Yes, Permanent: Yes
. 2024-11-30 20:28:58.184 Recycle bin: Delete to: No, Overwritten to: No, Bin path: 
. 2024-11-30 20:28:58.184 DST mode: Unix
. 2024-11-30 20:28:58.184 --------------------------------------------------------------------------
. 2024-11-30 20:28:58.252 Looking up host "localhost" for SSH connection
. 2024-11-30 20:28:58.267 Connecting to ::1 port 2222
. 2024-11-30 20:28:59.271 Failed to connect to ::1: Network error: Connection refused
. 2024-11-30 20:28:59.271 Connecting to 127.0.0.1 port 2222
. 2024-11-30 20:28:59.271 Connected to 127.0.0.1
. 2024-11-30 20:28:59.271 Selecting events 63 for socket 2008
. 2024-11-30 20:28:59.338 Waiting for the server to continue with the initialization
. 2024-11-30 20:28:59.338 Looking for incoming data
. 2024-11-30 20:28:59.338 Looking for network events
. 2024-11-30 20:28:59.338 We claim version: SSH-2.0-WinSCP_release_6.3.6
. 2024-11-30 20:28:59.338 Detected network event
. 2024-11-30 20:28:59.338 Enumerating network events for socket 2008
. 2024-11-30 20:28:59.338 Enumerated 19 network events making 19 cumulative events for socket 2008
. 2024-11-30 20:28:59.338 Handling network write event on socket 2008 with error 0
. 2024-11-30 20:28:59.338 Handling network connect event on socket 2008 with error 0
. 2024-11-30 20:28:59.338 Connected to 127.0.0.1
. 2024-11-30 20:28:59.338 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:28:59.338 Waiting for the server to continue with the initialization
. 2024-11-30 20:28:59.338 Looking for incoming data
. 2024-11-30 20:28:59.338 Looking for network events
. 2024-11-30 20:28:59.338 Remote version: SSH-2.0-paramiko_3.5.0
. 2024-11-30 20:28:59.338 Using SSH protocol version 2
. 2024-11-30 20:28:59.340 Have a known host key of type rsa2
. 2024-11-30 20:28:59.341 Detected network event
. 2024-11-30 20:28:59.341 Enumerating network events for socket 2008
. 2024-11-30 20:28:59.341 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:28:59.341 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:28:59.341 Waiting for the server to continue with the initialization
. 2024-11-30 20:28:59.341 Looking for incoming data
. 2024-11-30 20:28:59.341 Looking for network events
. 2024-11-30 20:28:59.341 Enabling strict key exchange semantics
. 2024-11-30 20:28:59.342 Doing ECDH key exchange with curve Curve25519, using hash SHA-256
. 2024-11-30 20:28:59.357 Detected network event
. 2024-11-30 20:28:59.357 Enumerating network events for socket 2008
. 2024-11-30 20:28:59.357 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:28:59.357 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:28:59.357 Waiting for the server to continue with the initialization
. 2024-11-30 20:28:59.357 Looking for incoming data
. 2024-11-30 20:28:59.357 Looking for network events
. 2024-11-30 20:28:59.385 Host key fingerprint is:
. 2024-11-30 20:28:59.385 ssh-rsa 2048 SHA256:+v5RA5YLQt/mgvJlKSdaL+WDRbZIzIfdKnSofO2EFs4
. 2024-11-30 20:28:59.385 Verifying host key rsa2 0x10001,0xa474f68505b945e5 12838cb4947becf3 712c77f7070b51b2 ada315a16c79a659 f28123915dc5fc82 4e7dceed33aea6a8 277ccc0936e4a6ee 00b098c48ecb25d4 df3ba51e0d4bec55 562df6d93ca77874 f44fd35858f90b1f ba8624904f0c5f50 3faab37435d0e92c 41f2eb21f50641f2 3b3f8828d7437305 00e518cf1d0b644c 3608d1b4934eb745 5a1e9be7de0ec591 cf2776baea0a3efa 2991ea5c5c97fe8b fea2da8b370bcae6 5d37b929a6363336 efa2384352906d5a 4734bd180b6bfceb a090b40cf94c5ae5 85350500314b954f 747b366d86f30042 fb24eb96790a0107 c7fb63869a8b9125 4ec30e3554d284e6 0ad1c062637ffb30 e63f71c73db05ca5  with fingerprints ssh-rsa 2048 SHA256:+v5RA5YLQt/mgvJlKSdaL+WDRbZIzIfdKnSofO2EFs4, ssh-rsa 2048 d8:d6:b8:b0:4c:52:7c:c3:25:68:4c:ad:c1:9a:09:a8
. 2024-11-30 20:28:59.416 Host key does not match cached key 0x10001,0xeb9df484cee32329 088fce52593d8ce5 1f6413cecadff70d 672fae868e9e1274 8426edf1affe8287 bb6a7dd0be675e53 b93db269cc6511de b55ae3110cc67d14 d7d3dca2f8330e92 6321f0ca7f315bd0 6fac4e10c86eb1b6 7a88d7699ddac355 9d23ecb8d2b267d3 05b73d26b76cb0e7 f8871ff31ee2258a cc60b0f8e2bf8095 6825929c0c16ff3c bc30b7f045eb05e4 3efc3660f5d7510d 7e6f73ef8afffc1e f3cebe1ff0cd3eac 05f24195067fc640 6d6a92d62a929a58 b0113d82acd54b5b 5d488fa1719ccb51 2dddb99efac8d955 794c3ca3ce72844d 36a3066986e5dee9 cf26a2d5f9dd1d1c 42db470f6b0f0246 d004a213c499ce9f b4aeb88fa7a595d9 
. 2024-11-30 20:28:59.417 Asking user:
. 2024-11-30 20:28:59.417 **Attenzione: potenziale violazione della sicurezza!**
. 2024-11-30 20:28:59.417 
. 2024-11-30 20:28:59.417 La chiave host non corrisponde a quella memorizzata nella cache da WinSCP per questo server:
. 2024-11-30 20:28:59.417 localhost (porta 2222)
. 2024-11-30 20:28:59.417 
. 2024-11-30 20:28:59.417 Ciò significa che l'amministratore del server ha modificato la chiave host, o ti sei effettivamente connesso ad un altro computer fingendo di essere il server.
. 2024-11-30 20:28:59.417 
. 2024-11-30 20:28:59.417     La firma digitale della chiave rsa2 è:
. 2024-11-30 20:28:59.417     ssh-rsa 2048 +v5RA5YLQt/mgvJlKSdaL+WDRbZIzIfdKnSofO2EFs4
. 2024-11-30 20:28:59.417 
. 2024-11-30 20:28:59.417 Se ti aspettavi questo cambiamento, fidati della nuova chiave e vuoi continuare a connetterti al server, seleziona Aggiorna per aggiornare la cache, oppure seleziona Aggiungi per aggiungere la nuova chiave alla cache mantenendo quelle vecchie. 
. 2024-11-30 20:28:59.417 Se vuoi continuare a connetterti ma senza aggiornare la cache, seleziona Connetti una volta.
. 2024-11-30 20:28:59.417 Se vuoi abbandonare completamente la connessione, seleziona Annulla per annullare. Selezionare Annulla è l'UNICA scelta sicura garantita. ()
. 2024-11-30 20:29:00.478 Answer: Yes
. 2024-11-30 20:29:00.479 Initialised AES-256 SDCTR (AES-NI accelerated) [aes256-ctr] outbound encryption
. 2024-11-30 20:29:00.479 Initialised HMAC-SHA-256 outbound MAC algorithm
. 2024-11-30 20:29:00.479 Initialised AES-256 SDCTR (AES-NI accelerated) [aes256-ctr] inbound encryption
. 2024-11-30 20:29:00.479 Initialised HMAC-SHA-256 inbound MAC algorithm
. 2024-11-30 20:29:00.479 Detected network event
. 2024-11-30 20:29:00.479 Enumerating network events for socket 2008
. 2024-11-30 20:29:00.479 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:00.479 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:00.480 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:00.480 Looking for incoming data
. 2024-11-30 20:29:00.480 Looking for network events
. 2024-11-30 20:29:00.482 Detected network event
. 2024-11-30 20:29:00.482 Enumerating network events for socket 2008
. 2024-11-30 20:29:00.482 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:00.482 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:00.482 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:00.483 Looking for incoming data
. 2024-11-30 20:29:00.483 Looking for network events
! 2024-11-30 20:29:00.485 Using username "MagoLione".
. 2024-11-30 20:29:00.512 Detected network event
. 2024-11-30 20:29:00.513 Enumerating network events for socket 2008
. 2024-11-30 20:29:00.513 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:00.513 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:00.513 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:00.513 Looking for incoming data
. 2024-11-30 20:29:00.513 Looking for network events
. 2024-11-30 20:29:00.513 Server offered these authentication methods: password
. 2024-11-30 20:29:00.513 Prompt (password, "SSH password", <no instructions>, "&Password: ")
. 2024-11-30 20:29:00.513 Using stored password.
. 2024-11-30 20:29:00.544 Sent password
. 2024-11-30 20:29:01.030 Detected network event
. 2024-11-30 20:29:01.030 Enumerating network events for socket 2008
. 2024-11-30 20:29:01.030 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:01.030 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:01.030 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:01.030 Looking for incoming data
. 2024-11-30 20:29:01.030 Looking for network events
. 2024-11-30 20:29:01.030 Access granted
. 2024-11-30 20:29:01.030 Opening main session channel
. 2024-11-30 20:29:01.031 Detected network event
. 2024-11-30 20:29:01.031 Enumerating network events for socket 2008
. 2024-11-30 20:29:01.031 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:01.031 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:01.031 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:01.031 Looking for incoming data
. 2024-11-30 20:29:01.031 Looking for network events
. 2024-11-30 20:29:01.031 Opened main channel
. 2024-11-30 20:29:01.108 Detected network event
. 2024-11-30 20:29:01.108 Enumerating network events for socket 2008
. 2024-11-30 20:29:01.108 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:01.108 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:01.108 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:01.108 Looking for incoming data
. 2024-11-30 20:29:01.108 Looking for network events
. 2024-11-30 20:29:01.108 Primary command failed; attempting fallback
. 2024-11-30 20:29:01.138 Detected network event
. 2024-11-30 20:29:01.138 Enumerating network events for socket 2008
. 2024-11-30 20:29:01.138 Enumerated 1 network events making 1 cumulative events for socket 2008
. 2024-11-30 20:29:01.138 Handling network read event on socket 2008 with error 0
. 2024-11-30 20:29:01.138 Waiting for the server to continue with the initialization
. 2024-11-30 20:29:01.138 Looking for incoming data
. 2024-11-30 20:29:01.138 Looking for network events
. 2024-11-30 20:29:01.138 Started a shell/command
. 2024-11-30 20:29:01.138 Timeout waiting for network events
. 2024-11-30 20:29:01.173 --------------------------------------------------------------------------
. 2024-11-30 20:29:01.173 Using SCP protocol.
. 2024-11-30 20:29:01.173 Doing startup conversation with host.
. 2024-11-30 20:29:01.173 Session upkeep
. 2024-11-30 20:29:01.173 Looking for network events
. 2024-11-30 20:29:01.173 Timeout waiting for network events
. 2024-11-30 20:29:01.191 Skipping host startup message (if any).
> 2024-11-30 20:29:01.191 echo "WinSCP: this is end-of-file:0"
. 2024-11-30 20:29:01.191 Sent 37 bytes
. 2024-11-30 20:29:01.191 There are 0 bytes remaining in the send buffer
. 2024-11-30 20:29:01.191 Looking for network events
. 2024-11-30 20:29:01.191 Timeout waiting for network events
. 2024-11-30 20:29:01.191 Waiting for another 1 bytes
. 2024-11-30 20:29:01.191 Looking for incoming data
. 2024-11-30 20:29:01.191 Looking for network events
. 2024-11-30 20:29:17.465 Timeout waiting for network events
. 2024-11-30 20:29:17.465 Waiting for data timed out, asking user what to do.
. 2024-11-30 20:29:17.465 Asking user:
. 2024-11-30 20:29:17.465 **L'host non sta comunicando da 15 secondi.
. 2024-11-30 20:29:17.465 
. 2024-11-30 20:29:17.465  Attendi per altri 15 secondi?** ()
. 2024-11-30 20:29:17.465 Session upkeep
. 2024-11-30 20:29:17.993 Pooling for data in case they finally arrives
. 2024-11-30 20:29:17.993 Looking for network events
. 2024-11-30 20:29:17.993 Timeout waiting for network events
. 2024-11-30 20:29:18.474 Session upkeep
. 2024-11-30 20:29:18.489 Pooling for data in case they finally arrives
. 2024-11-30 20:29:18.489 Looking for network events
. 2024-11-30 20:29:18.490 Timeout waiting for network events
. 2024-11-30 20:29:18.998 Pooling for data in case they finally arrives
. 2024-11-30 20:29:18.998 Looking for network events
. 2024-11-30 20:29:18.998 Timeout waiting for network events
. 2024-11-30 20:29:19.477 Session upkeep
. 2024-11-30 20:29:19.493 Pooling for data in case they finally arrives
. 2024-11-30 20:29:19.493 Looking for network events
. 2024-11-30 20:29:19.493 Timeout waiting for network events
. 2024-11-30 20:29:19.989 Pooling for data in case they finally arrives
. 2024-11-30 20:29:19.989 Looking for network events
. 2024-11-30 20:29:19.989 Timeout waiting for network events
. 2024-11-30 20:29:20.479 Answer: Abort
. 2024-11-30 20:29:20.479 Attempt to close connection due to fatal exception:
* 2024-11-30 20:29:20.479 **Terminato dall'utente.**
. 2024-11-30 20:29:20.479 Closing connection.
. 2024-11-30 20:29:20.479 Sending special code: 1
. 2024-11-30 20:29:20.479 Looking for network events
. 2024-11-30 20:29:20.580 Timeout waiting for network events
. 2024-11-30 20:29:20.580 Looking for network events
. 2024-11-30 20:29:20.690 Timeout waiting for network events
. 2024-11-30 20:29:20.690 Looking for network events
. 2024-11-30 20:29:20.799 Timeout waiting for network events
. 2024-11-30 20:29:20.799 Looking for network events
. 2024-11-30 20:29:20.907 Timeout waiting for network events
. 2024-11-30 20:29:20.907 Looking for network events
. 2024-11-30 20:29:21.017 Timeout waiting for network events
. 2024-11-30 20:29:21.017 Selecting events 0 for socket 2008
* 2024-11-30 20:29:21.075 (EFatal) **Terminato dall'utente.**
* 2024-11-30 20:29:21.075 Errore dis alto del messaggio iniziale. L'interprete dei comandi probabilmente è incompatibile con l'applicazione (si raccomanda BASH).

Maybe is a problem of shell-command management. I didn't find any tutorial.


Solution

  • You need to add ssh_server = SSHServer() outside of all functions and classes to ensure that only one instance of the SSHServer is used. Consequently, you must remove its creation from the handle_client() and start_sftp_server() functions.

    The corrected handle_client() function is as follows (without creating an SSHServer instance):

    def handle_client(client_socket: socket.socket, host_key: paramiko.RSAKey):
        try:
            
            transport = paramiko.Transport(client_socket)
            transport.add_server_key(host_key)
            
            transport.set_subsystem_handler('sftp', paramiko.SFTPServer, SFTPServer)
    
            try:
                transport.start_server(server=ssh_server)
            except paramiko.SSHException as e:
                logging.error(f"SSH negotiation failed: {e}")
                return
    
            channel = transport.accept(40)
            if channel is None:
                logging.error("No channel found")
                return
    
            logging.info("Client authenticated.")
            logging.debug("Starting SFTP session.")
    
            while not channel.closed:
                time.sleep(1)
            
        except paramiko.SSHException as e:
            logging.error(f"SSH exception: {e}")
        except Exception as e:
            logging.error(f"Error handling client: {e}")
        finally:
            logging.debug("Closing channel.")
            if channel != None:
                channel.close()
    

    Before, the socket was correctly bound and set up, but you need to remove transport operations (as they are handled in the handle_client() function).

    The corrected start_sftp_server() function is:

    def start_sftp_server():
        logging.info("Starting SFTP server...")
        
        host_key = paramiko.RSAKey.generate(2048)
        
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.bind(('localhost', 2222))
        server_socket.listen(100)
        
        while True:
            connection_socket, _ = server_socket.accept()
            threading.Thread(target=handle_client, args=[connection_socket, host_key]).start()
    

    The while loop is used to accept connection requests, and for each connection, it starts handle_client() in a new thread.

    You also need to implement the functions needed to read and write files in SFTPServer.

    To do that, first you have to create an SFTPHandle class, that exends paramiko.SFTPHandle:

        class SFTPHandle (paramiko.SFTPHandle):
            def stat(self):
                try:
                    return paramiko.SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
                except OSError as e:
                    return paramiko.SFTPServer.convert_errno(e.errno)
        
            def chattr(self, attr):
                try:
                    paramiko.SFTPServer.set_file_attr(self.filename, attr)
                    return paramiko.SFTP_OK
                except OSError as e:
                    return paramiko.SFTPServer.convert_errno(e.errno)
    
    This is usefull to handle files operations.
    
    Then, there is the correct `SFTPServer` class: 
    class SFTPServer(paramiko.SFTPServerInterface):
        ROOT = ROOT_FOLDER
        KEY = None
    
        def _realpath(self, path):
            return self.ROOT + self.canonicalize(path)
    
        def list_folder(self, path):
            path = self._realpath(path)
            try:
                out = []
                flist = os.listdir(path)
                for fname in flist:
                    attr = paramiko.SFTPAttributes.from_stat(os.lstat(os.path.join(path, fname)))
                    attr.filename = fname
                    out.append(attr)
                return out
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
    
        def stat(self, path):
            path = self._realpath(path)
            try:
                return paramiko.SFTPAttributes.from_stat(os.stat(path))
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
    
        def lstat(self, path):
            path = self._realpath(path)
            try:
                return paramiko.SFTPAttributes.from_stat(os.lstat(path))
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
    
        def open(self, path, flags, attr):
            path = self._realpath(path)
            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:
                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:
                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):
            path = self._realpath(path)
            try:
                os.remove(path)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK
    
        def rename(self, oldpath, newpath):
            oldpath = self._realpath(oldpath)
            newpath = self._realpath(newpath)
            try:
                os.rename(oldpath, newpath)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK
    
        def mkdir(self, path, attr):
            path = self._realpath(path)
            try:
                os.mkdir(path)
                if attr is not None:
                    paramiko.SFTPServer.set_file_attr(path, attr)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK
    
        def rmdir(self, path):
            path = self._realpath(path)
            try:
                os.rmdir(path)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK
    
        def chattr(self, path, attr):
            path = self._realpath(path)
            try:
                paramiko.SFTPServer.set_file_attr(path, attr)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK
    
        def symlink(self, target_path, path):
            path = self._realpath(path)
            if (len(target_path) > 0) and (target_path[0] == '/'):
                # absolute symlink
                target_path = os.path.join(self.ROOT, target_path[1:])
                if target_path[:2] == '//':
                    # bug in os.path.join
                    target_path = target_path[1:]
            else:
                # compute relative to path
                abspath = os.path.join(os.path.dirname(path), target_path)
                if abspath[:len(self.ROOT)] != self.ROOT:
                    # this symlink isn't going to work anyway -- just break it immediately
                    target_path = '<error>'
            try:
                os.symlink(target_path, path)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            return paramiko.SFTP_OK
    
        def readlink(self, path):
            path = self._realpath(path)
            try:
                symlink = os.readlink(path)
            except OSError as e:
                return paramiko.SFTPServer.convert_errno(e.errno)
            # if it's absolute, remove the root
            if os.path.isabs(symlink):
                if symlink[:len(self.ROOT)] == self.ROOT:
                    symlink = symlink[len(self.ROOT):]
                    if (len(symlink) == 0) or (symlink[0] != '/'):
                        symlink = '/' + symlink
                else:
                    symlink = '<error>'
            return symlink
    

    You can get some inspiration from this great example of a small sftp server.