# Writing a client with Twisted.Conch

## Introduction

In the original days of computing, rsh/rlogin were used to connect to remote computers and execute commands. These commands had the problem that the passwords and commands were sent in the clear. To solve this problem, the SSH protocol was created. Twisted.Conch implements the second version of this protocol.

## Writing a client

Writing a client with Conch involves sub-classing 4 classes: `twisted.conch.ssh.transport.SSHClientTransport`, `twisted.conch.ssh.userauth.SSHUserAuthClient`, `twisted.conch.ssh.connection.SSHConnection`, and `twisted.conch.ssh.channel.SSHChannel`. We'll start out with SSHClientTransport because it's the base of the client.

## The Transport

    from twisted.conch import error
    from twisted.conch.ssh import transport
    from twisted.internet import defer

    class ClientTransport(transport.SSHClientTransport):

        def verifyHostKey(self, pubKey, fingerprint):
            if fingerprint != 'b1:94:6a:c9:24:92:d2:34:7c:62:35:b4:d2:61:11:84':
                return defer.fail(error.ConchError('bad key'))
            else:
                return defer.succeed(1)

        def connectionSecure(self):
            self.requestService(ClientUserAuth('user', ClientConnection()))

See how easy it is? SSHClientTransport handles the negotiation of encryption and the verification of keys for you. The one security element that you as a client writer need to implement is verifyHostKey(). This method is called with two strings: the public key sent by the server and its fingerprint. You should verify the host key the server sends, either by checking against a hard-coded value as in the example, or by asking the user. verifyHostKey returns a `twisted.internet.defer.Deferred` which gets a callback if the host key is valid, or an errback if it is not.

The second method you need to implement is connectionSecure(). It is called when the encryption is set up and other services can be run. The example requests that the ClientUserAuth service be started. This service will be discussed next.

## The Authorization Client

    from twisted.conch.ssh import keys, userauth

    # these are the public/private keys from test_conch

    publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3\
    /c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHR\
    ivcJSkbh/C+BR3utDS555mV'

    privateKey = """-----BEGIN RSA PRIVATE KEY-----
    MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW
    4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw
    vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb
    Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1
    xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8
    PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2
    gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu
    DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML
    pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP
    EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg==
    -----END RSA PRIVATE KEY-----"""

    class ClientUserAuth(userauth.SSHUserAuthClient):

        def getPassword(self, prompt = None):
            return
            # this says we won't do password authentication

        def getPublicKey(self):
            return keys.getPublicKeyString(data = publicKey)

        def getPrivateKey(self):
            return defer.succeed(keys.getPrivateKeyObject(data = privateKey))

Again, fairly simple. The SSHUserAuthClient takes care of most of the work, but the actual authentication data needs to be supplied. getPassword() asks for a password, getPublicKey() and getPrivateKey() get public and private keys, respectively. getPassword() returns a Deferred that is called back with the password to use. getPublicKey() returns the SSH key data for the public key to use. keys.getPublicKeyString() will take keys in OpenSSH and LSH format, and convert them to the required format. getPrivateKey() returns a Deferred which is called back with the key object (as used in PyCrypto) for the private key. getPassword() and getPrivateKey() return Deferreds because they may need to ask the user for input.

Once the authentication is complete, SSHUserAuthClient takes care of starting the SSHConnection object given to it. Next, we'll look at how to use the SSHConnection.

## The Connection

    from twisted.conch.ssh import connection

    class ClientConnection(connection.SSHConnection):

        def serviceStarted(self):
            self.openChannel(CatChannel(conn = self))

SSHConnection is the easiest, as it's only responsible for starting the channels. It has other methods, those will be examined when we look at SSHChannel

## The Channel

    from twisted.conch.ssh import channel, common

    class CatChannel(channel.SSHChannel):

        name = 'session'

        def channelOpen(self, data):
            d = self.conn.sendRequest(self, 'exec', common.NS('cat'),
                                      wantReply = 1)
            d.addCallback(self._cbSendRequest)
            self.catData = ''

        def _cbSendRequest(self, ignored):
            self.write('This data will be echoed back to us by "cat."\r\n')
            self.sendEOF()
            self.loseConnection()

        def dataReceived(self, data):
            self.catData += data

        def closed(self):
            print 'We got this from "cat":', self.catData

Now that we've spent all this time getting the server and client connected, here is where that work pays off. SSHChannel is the interface between you and the other side. This particular channel opens a session and plays with the 'cat' program, but your channel can implement anything, so long as the server supports it.

The channelOpen() method is where everything gets started. It gets passed a chunk of data; however, this chunk is usually nothing and can be ignored. Our channelOpen() initializes our channel, and sends a request to the other side, using the sendRequest() method of the SSHConnection object. Requests are used to send events to the other side. We pass the method self so that it knows to send the request for this channel. The 2nd argument of 'exec' tells the server that we want to execute a command. The third argument is the data that accompanies the request. `common.NS` encodes the data as a length-prefixed string, which is how the server expects the data. We also say that we want a reply saying that the process has a been started. sendRequest() then returns a Deferred which we add a callback for.

Once the callback fires, we send the data. SSHChannel supports the `twisted.internet.interface.Transport` interface, so it can be given to Protocols to run them over the secure connection. In our case, we just write the data directly. sendEOF() does not follow the interface, but Conch uses it to tell the other side that we will write no more data. loseConnection() shuts down our side of the connection, but we will still receive data through dataReceived(). The closed() method is called when both sides of the connection are closed, and we use it to display the data we received (which should be the same as the data we sent.)

Index