12

I am trying to implement a very simple file transfer client in python using twisted conch. The client should simply transfer a few files to a remote ssh/sftp server in a programatic way. The function is given username, password, file list, destination server:directory and just needs to carry out the authentication and copying in a cross-platform way.

I have read some introductory material on twisted and have managed to make my own SSH client which just executes cat on the remote server. I am having a real difficult time extending this to moving the files over. I have taken a look at cftp.py and the filetransfer tests but am completely mystified by twisted.

Does anyone have any suggestions or references that can point me in the right direction? The SSH client I have constructed already is based off this one.

rymurr
  • 444
  • 4
  • 11
  • 1
    Can you explain how you're stuck more specifically? As your question is now, the only way I can think of to answer it is to write a complete Conch/SFTP tutorial, which is probably more work than 15 points on SO is worth (at least at the moment). ;) But a more specific question might have a simpler answer. – Jean-Paul Calderone Mar 04 '11 at 16:17
  • @Jean-Paul Right now I _think_ I need to subclass t.c.s.f.FileTransferClient. I also _think_ that I need to open up a SSH connection similar to the example I have linked above. I am stuck with how to properly subclass t.c.s.f.FileTransferClient and how to actually move the files over. A full blown tutorial is not necessary as I am interested in learning twisted(this was my first small project) but a sketch of which methods and classes I should be using or reading about or even a simple-ish example in the docs(i found cftp.py to hard to read) would be much appreciated. – rymurr Mar 04 '11 at 16:50

2 Answers2

36

Doing an SFTP file transfer with Twisted Conch involves a couple distinct phases (well, they're distinct if you squint). Basically, first you need to get a connection set up with a channel open on it with an sftp subsystem running on it. Whew. Then you can use the methods of a FileTransferClient instance connected to that channel to perform whichever SFTP operations you want to perform.

The bare essentials of getting an SSH connection set up can be taken care of for you by APIs provided by modules in the twisted.conch.client package. Here's a function that wraps up the slight weirdness of twisted.conch.client.default.connect in a slightly less surprising interface:

from twisted.internet.defer import Deferred
from twisted.conch.scripts.cftp import ClientOptions
from twisted.conch.client.connect import connect
from twisted.conch.client.default import SSHUserAuthClient, verifyHostKey

def sftp(user, host, port):
    options = ClientOptions()
    options['host'] = host
    options['port'] = port
    conn = SFTPConnection()
    conn._sftp = Deferred()  
    auth = SSHUserAuthClient(user, options, conn)
    connect(host, port, options, verifyHostKey, auth)
    return conn._sftp

This function takes a username, hostname (or IP address), and port number and sets up an authenticated SSH connection to the server at that address using the account associated with the given username.

Actually, it does a little more than that, because the SFTP setup is a little mixed in here. For the moment though, ignore SFTPConnection and that _sftp Deferred.

ClientOptions is basically just a fancy dictionary that connect wants to be able to see what it's connecting to so it can verify the host key.

SSHUserAuthClient is the object that defines how the authentication will happen. This class knows how to try the usual things like looking at ~/.ssh and talking to a local SSH agent. If you want to change how authentication is done, this is the object to play around with. You can subclass SSHUserAuthClient and override its getPassword, getPublicKey, getPrivateKey, and/or signData methods, or you can write your own completely different class that has whatever other authentication logic you want. Take a look at the implementation to see what methods the SSH protocol implementation calls on it to get the authentication done.

So this function will set up an SSH connection and authenticate it. After that's done, the SFTPConnection instance comes into play. Notice how SSHUserAuthClient takes the SFTPConnection instance as an argument. Once authentication succeeds, it hands off control of the connection to that instance. In particular, that instance has serviceStarted called on it. Here's the full implementation of the SFTPConnection class:

class SFTPConnection(SSHConnection):
    def serviceStarted(self):
        self.openChannel(SFTPSession())

Very simple: all it does is open a new channel. The SFTPSession instance it passes in gets to interact with that new channel. Here's how I defined SFTPSession:

class SFTPSession(SSHChannel):
    name = 'session'

    def channelOpen(self, whatever):
        d = self.conn.sendRequest(
            self, 'subsystem', NS('sftp'), wantReply=True)
        d.addCallbacks(self._cbSFTP)


    def _cbSFTP(self, result):
        client = FileTransferClient()
        client.makeConnection(self)
        self.dataReceived = client.dataReceived
        self.conn._sftp.callback(client)

Like with SFTPConnection, this class has a method that gets called when the connection is ready for it. In this case, it's called when the channel is opened successfully, and the method is channelOpen.

At last, the requirements for launching the SFTP subsystem are in place. So channelOpen sends a request over the channel to launch that subsystem. It asks for a reply so it can tell when that has succeeded (or failed). It adds a callback to the Deferred it gets to hook up a FileTransferClient to itself.

The FileTransferClient instance will actually format and parse bytes that move over this channel of the connection. In other words, it is an implementation of just the SFTP protocol. It is running over the SSH protocol, which the other objects this example has created take care of. But as far as it is concerned, it receives bytes in its dataReceived method, parses them and dispatches data to callbacks, and it offers methods which accept structured Python objects, formats those objects as the right bytes, and writes them to its transport.

None of that is directly important to using it, though. However, before giving an example of how to perform SFTP actions with it, let's cover that _sftp attribute. This is my crude approach to making this newly connected FileTransferClient instance available to some other code which will actually know what to do with it. Separating the SFTP setup code from the code that actually uses the SFTP connection makes it easier to reuse the former while changing the latter.

So the Deferred I set in sftp gets fired with the FileTransferClient connected in _cbSFTP. And the caller of sftp got that Deferred returned to them, so that code can do things like this:

def transfer(client):
    d = client.makeDirectory('foobarbaz', {})
    def cbDir(ignored):
        print 'Made directory'
    d.addCallback(cbDir)   
    return d


def main():
    ...
    d = sftp(user, host, port)
    d.addCallback(transfer)

So first sftp sets up the whole connection, all the way to connecting a local FileTransferClient instance up to a byte stream which has some SSH server's SFTP subsystem on the other end, and then transfer takes that instance and uses it to make a directory, using one of the methods of FileTransferClient for performing some SFTP operation.

Here's a complete code listing that you should be able to run and to see a directory created on some SFTP server:

from sys import stdout

from twisted.python.log import startLogging, err

from twisted.internet import reactor
from twisted.internet.defer import Deferred

from twisted.conch.ssh.common import NS
from twisted.conch.scripts.cftp import ClientOptions
from twisted.conch.ssh.filetransfer import FileTransferClient
from twisted.conch.client.connect import connect
from twisted.conch.client.default import SSHUserAuthClient, verifyHostKey
from twisted.conch.ssh.connection import SSHConnection
from twisted.conch.ssh.channel import SSHChannel


class SFTPSession(SSHChannel):
    name = 'session'

    def channelOpen(self, whatever):
        d = self.conn.sendRequest(
            self, 'subsystem', NS('sftp'), wantReply=True)
        d.addCallbacks(self._cbSFTP)


    def _cbSFTP(self, result):
        client = FileTransferClient()
        client.makeConnection(self)
        self.dataReceived = client.dataReceived
        self.conn._sftp.callback(client)



class SFTPConnection(SSHConnection):
    def serviceStarted(self):
        self.openChannel(SFTPSession())


def sftp(user, host, port):
    options = ClientOptions()
    options['host'] = host
    options['port'] = port
    conn = SFTPConnection()
    conn._sftp = Deferred()
    auth = SSHUserAuthClient(user, options, conn)
    connect(host, port, options, verifyHostKey, auth)
    return conn._sftp


def transfer(client):
    d = client.makeDirectory('foobarbaz', {})
    def cbDir(ignored):
        print 'Made directory'
    d.addCallback(cbDir)
    return d


def main():
    startLogging(stdout)

    user = 'exarkun'
    host = 'localhost'
    port = 22
    d = sftp(user, host, port)
    d.addCallback(transfer)
    d.addErrback(err, "Problem with SFTP transfer")
    d.addCallback(lambda ignored: reactor.stop())
    reactor.run()


if __name__ == '__main__':
    main()

makeDirectory is a fairly simple operation. The makeDirectory method returns a Deferred that fires when the directory has been created (or if there's an error doing so). Transferring a file is a little more involved, because you have to supply the data to send, or define how received data will be interpreted if you're downloading instead of uploading.

If you read the docstrings for the methods of FileTransferClient, though, you should see how to use its other features - for actual file transfer, openFile is mainly of interest. It gives you a Deferred which fires with an ISFTPFile provider. This object has methods for reading and writing file contents.

Jean-Paul Calderone
  • 47,755
  • 6
  • 94
  • 122
  • Thanks very much for the tutorial. It helped a lot and things are MUCH clearer now. I now have everything working! I look forward to doing more stuff with twisted in the future – rymurr Mar 06 '11 at 15:41
  • Great explanation, but can you elaborate on the `self.dataReceived = client.dataReceived`? – daf Apr 04 '12 at 21:44
0

SSH clients isn't something independent from other OS services. Do you really want to add support to .ssh folders, keychains, etc? May be more quick and robust way is make wrapper around scp (Linux, OSX) and pscp under Windows. And this way looks more "Linux way" (chain existing small pieces into something complex).

Mikhail Kashkin
  • 1,521
  • 14
  • 29
  • 2
    My understanding of twisted and conch is that you can implement SSH services independent of the OS. `.ssh` folders and such are not important for what I am looking to do. A remote gui is just sending a script and some parameters to a cluster all in a secured network so it doesn't need to be overly secure. The only way to the cluster is through SSH though. – rymurr Mar 04 '11 at 15:25